20191202のRubyに関する記事は29件です。

誤り検出方法 checksumについて

ブロックチェーン関連の実装を行なっていた際に、チェックサムで引っかかり「アドレスがinvalidです」と表示されることがあった。その際に、「チェックサムって誤り検出のやつだよな?仕組み知らないわ」となったので、少し調べて簡易的に実装してみた。

チャックサムとは?

チャックサムとは、データの信頼性を検査するための算出した値のことを言う。この値を元に、データが適切なものであるかを調べることができる

非常に単純な誤り検出方法の1つで、誤り検出の精度は低いが原理が簡単で容易に実装でき、計算コストも低いため、簡単な誤り検出方法として広く普及している

データの誤り検出の方法としてハッシュ関数を利用する方法もあるが、誤り検出のために送信する必要のあるデータのサイズが大きく、処理負荷が大きい。しかし、誤り検出の信頼性はチャックサムを利用する場合よりも高い。

チェックサムの求め方

チャックサムの求め方については、様々な手法があるが簡単な方法としては、データ列を整数値の列としてみなし和を求め、これをある定数で割った余りを検査用データとするものがある。

チャックサムの計算の例

[0xc0 0xA8 0xFF 0x58] から1バイトのチェックサムを算出するのは、
0xc0+0xA8 +0xFF+0x58 = 0x2BFを求め、0x100で割った余りの0xBFがチャックサムになる

データのサイズが大きい場合は、一定の大きさごとに区切ってチャックサムを算出付加する方法が利用される

チャックサムを利用した誤り検知の実装

文字列に関してチェックサムを利用したデータの作成の関数と、誤り検出を行う関数を簡易的に実装した。

チャックサムを付加したデータを作る関数

文字列を整数値に変換して、その和を100で割ったものをチャックサムにし、データに付加して送信用のデータを作成する関数

def make_data(raw_data)
    checksum = 0
    raw_data.unpack("C*").each do |byte|
        checksum += byte
    end
    return raw_data.unpack("C*").push(checksum / 100)
end

付加されたチェックサムを確認する関数

受信したデータのチャックサムを確認し、適正なデータであれば表示し、データが破損していれば、その旨を表示する関数

def check_data(data)
    sent_checksum = data.pop
    checksum = 0
    data.each do |byte|
        checksum += byte
    end
    return sent_checksum == checksum / 100
end

def show_data(hex_data)
    data = ""
    if(check_data(hex_data)) then
        hex_data.each do |byte|
            data += byte.chr
        end
        p data + ":データは破損していません"
    else 
        p "データが破損しています"
    end
end

利用例

raw_data = "Hello"
send_data = make_data(raw_data)

show_data(send_data) // => "Hello:データは破損していません"


raw_data = "Hello"
send_data = make_data(raw_data)

send_data[1] = 0 //通信経路で意図的にデータを書き換える

show_data(send_data) // =>"データが破損しています"

今回の例では、利用できるchecksumは0から99の100通りである。そのため、通信経路で改ざんや破損があった際に、必ず検出することができるとは限らない。和を割る数を100から大きいものにすれば、誤りを検出できる確率は高くなるが、検出に必要なデータサイズが大きくなってしまう。そうなってしまうと、データのハッシュ値を利用する方がより信頼性の高い誤り検出ができる。あくまで簡易的な誤り検出であることがわかった。

また、アルゴリズム的にもチャックサムを用いる方法では矛盾が生じないような改ざんも行うことができる。これに対して、ハッシュ関数では基本的にこのようなことを行うことができない。

まとめ

個人的に気になったことを簡単にまとめた。誤り検出に関しての知識がほとんどないので、これを機に理解を深めたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

誤り検出方法 Checksumについて

ブロックチェーン関連の実装を行なっていた際に、チェックサムで引っかかり「アドレスがinvalidです」と表示されることがあった。その際に、「チェックサムって誤り検出のやつだよな?仕組み知らないわ」となったので、少し調べて簡易的に実装してみた。

チャックサムとは?

チャックサムとは、データの信頼性を検査するための算出した値のことを言う。この値を元に、データが適切なものであるかを調べることができる

非常に単純な誤り検出方法の1つで、誤り検出の精度は低いが原理が簡単で容易に実装でき、計算コストも低いため、簡単な誤り検出方法として広く普及している

データの誤り検出の方法としてハッシュ関数を利用する方法もあるが、誤り検出のために送信する必要のあるデータのサイズが大きく、処理負荷が大きい。しかし、誤り検出の信頼性はチャックサムを利用する場合よりも高い。

チェックサムの求め方

チャックサムの求め方については、様々な手法があるが簡単な方法としては、データ列を整数値の列としてみなし和を求め、これをある定数で割った余りを検査用データとするものがある。

チャックサムの計算の例

[0xc0 0xA8 0xFF 0x58] から1バイトのチェックサムを算出するのは、
0xc0+0xA8 +0xFF+0x58 = 0x2BFを求め、0x100で割った余りの0xBFがチャックサムになる

データのサイズが大きい場合は、一定の大きさごとに区切ってチャックサムを算出付加する方法が利用される

チャックサムを利用した誤り検知の実装

文字列に関してチェックサムを利用したデータの作成の関数と、誤り検出を行う関数を簡易的に実装した。

チャックサムを付加したデータを作る関数

文字列を整数値に変換して、その和を100で割ったものをチャックサムにし、データに付加して送信用のデータを作成する関数

def make_data(raw_data)
    checksum = 0
    raw_data.unpack("C*").each do |byte|
        checksum += byte
    end
    return raw_data.unpack("C*").push(checksum / 100)
end

付加されたチェックサムを確認する関数

受信したデータのチャックサムを確認し、適正なデータであれば表示し、データが破損していれば、その旨を表示する関数

def check_data(data)
    sent_checksum = data.pop
    checksum = 0
    data.each do |byte|
        checksum += byte
    end
    return sent_checksum == checksum / 100
end

def show_data(hex_data)
    data = ""
    if(check_data(hex_data)) then
        hex_data.each do |byte|
            data += byte.chr
        end
        p data + ":データは破損していません"
    else 
        p "データが破損しています"
    end
end

利用例

raw_data = "Hello"
send_data = make_data(raw_data)

show_data(send_data) // => "Hello:データは破損していません"


raw_data = "Hello"
send_data = make_data(raw_data)

send_data[1] = 0 //通信経路で意図的にデータを書き換える

show_data(send_data) // =>"データが破損しています"

今回の例では、利用できるchecksumは0から99の100通りである。そのため、通信経路で改ざんや破損があった際に、必ず検出することができるとは限らない。和を割る数を100から大きいものにすれば、誤りを検出できる確率は高くなるが、検出に必要なデータサイズが大きくなってしまう。そうなってしまうと、データのハッシュ値を利用する方がより信頼性の高い誤り検出ができる。あくまで簡易的な誤り検出であることがわかった。

また、アルゴリズム的にもチャックサムを用いる方法では矛盾が生じないような改ざんも行うことができる。これに対して、ハッシュ関数では基本的にこのようなことを行うことができない。

まとめ

個人的に気になったことを簡単にまとめた。誤り検出に関しての知識がほとんどないので、これを機に理解を深めたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Capybaraで壊れにくいテストを書くために気を付けていること

この記事はSmartHR Advent Calendar 2019 2日目の記事です。

SmartHRではRuby on Railsを広く採用しています。アプリケーションを長期的にメンテナンスしていくためにテストは欠かせません。特にReact.jsなどを用いた複雑なUIにおいては、単なるAPIのテストやモデルのテストだけではなく「実際にブラウザを操作して、ユーザーが期待する結果を得られるかどうか」をテストすることが重要です。

Rubyではこのようなブラウザを操作するテストを書くために、Capybaraという便利なフレームワークがあり、比較的簡単にテストを書き始めることができます。ただ、この手のテストは保守が大変であったり、手間の大きさからテストが追加されなくなったり、ということがよくあります。本記事では、私がこれまでの経験から学んだ、壊れにくいテストを書くためのTipsを紹介します。

なお、特に説明のない限り、ここではCapybara + RSpec + Selenium + Chrome (Headless)の環境を想定しています。

sleepしない

出オチっぽいですが、非常に重要です。非同期なリクエストの結果を待っているときや、時間差でレンダリングされる画面など「いい感じに少し待って」と言いたくなる状況は確かにあります。

そういった場合に、単にsleep 5などとしてしまうと

  • テストを実行する環境によって適切な待ち時間が異なるため、落ちたり落ちなかったりするテストが生まれやすい
  • テストが落ちたときに、適当に待ち時間を伸ばされることが多く、テストの実行時間が伸びがち

などの問題があります。このような場合では、何を待っているかを短いスパンで定期的にタイムアウトまで待つようなヘルパーメソッドを定義して、それを利用するようにしましょう。

it "何かのアクションの結果、Successメッセージが帰ってくる" do
  click_button "何かのアクション"
  finally do
    expect(page).to have_content "Success!"
  end
end

def finally(timeout: Capybara.default_max_wait_time)
  start = Time.now

  begin
    yield
    return
  rescue RSpec::Expectations::ExpectationNotMetError, Capybara::ElementNotFound
    raise if Time.now > start + timeout
    sleep 0.1
    retry
  end
end

XPathやclassに依存しない

Capybaraではhave_buttonfill_inなど基本的なHTMLの要素に対するヘルパーが定義されているため、素直な画面に対しては比較的読みやすいテストを書くことができます。

しかし、現実には画面が複雑な構造になっていることが多く、これらのヘルパーだけでは力不足であることはよくあります。そういったときに、XPathやclassに依存したテストを書いてしまうこともきっとよくあるでしょう。

find('form active-form button').click
expect(page).to have_xpath '//*[@id="form"]/div[2]'

しかし、これでは後からテストだけ見たときに、何をテストしているのかわからなくなってしまいます。例えば、あなたが画面を大幅に弄った後に、「よーし、テスト直すかー」とこのテストを見たら... きっとこのテストごと消えてしまうことになるでしょう。

このような悲劇を産まないためにも、テストは後から読めるように、XPathやclassに依存しないことをおすすめしています。とはいえ、素直にヘルパーが利用できない画面というのは当然ありますから、話はそんな簡単ではありません。こういった場合にはデータ属性を利用し、さらにそれを指定するヘルパーメソッドを生やすと良い感じになります。

click_form_button
expect_to_have_error_message
def click_form_button
  find(spec_selector('active-form-button')).click
end

def expect_to_have_error_message
  expect(page).to have spec_selector('active-form-error-message')
end

def spec_selector(name)
  "[data-spec='#{name}']"
end

データ属性は他の用途に利用されることがなく、自由に目印をつけられるので、テストを見たときに何を指しているかわかりやすく、HTML側の編集時にも目を引く良い方法です。適切な単位でデータ属性を割り当てていれば「なぜかわからないけどdivをひとつズラしたらテストが通らなくなった」といった問題も起きにくくなるでしょう。

withinを活用する

この記事をテストすると仮定して、「本文の書き出しに"Capybara"というリンクが含まれていること」をテストするとします。

expect(page).to have_link "Capybara", href: "https://github.com/teamcapybara/capybara"

これでもテストは通りますが、これでは「本文の書き出しの中に」という重要な条件が抜けてしまっています。例えば、末尾の参考文献に"Capybara"を含むリンクを追加した途端、本当にテストしたかった書き出しのリンクが消えても、テストが通る状態になってしまいます。

他にも「保存」ボタンをクリックしたい状況があるとして、同じ画面中にいくつも「保存」ボタンがあると、単にclick_button "保存"では、Ambiguous matchを引き起こしてしまいます。match: :firstallしてアクセスする方法もありますが、あまり良い方法ではありませんよね。

click_button "保存", match: :first # firstって何?
all('button', text: "保存")[1].click # うーん...

こういった場合では、withinによるスコープの絞り込みが役に立ちます。

it "書き出しにリンクが含まれる" do 
  within_introduction do
    expect(page).to have_link "Capybara", href: "https://github.com/teamcapybara/capybara"
  end
end

it "ヘッダーの保存ボタンをクリック" do
  within_header do
    click_button "保存"
  end
end

def within_introduction
  within(spec_selector("introduction")) { yield }
end

def within_header
  within(spec_selector("header")) { yield }
end

データ属性を使ったヘルパーメソッドの定義と合わせると、随分と読みやすく感じるはずです。

ユーザーの目に見えないもの(気にしないもの)をテストしない

これは書き方というか、心構えの問題だと思うのですが、基本的にデータベースの中身だったり、DOMの構造など「ユーザーが意識しないもの」はテストするべきではない、と考えています。例えば、こんなテストです。

visit edit_user_path(user)
fill_in "名前", with: "新しい名前"
click_button "保存"

expect(user).to have_attribute(name: "新しい名前")

もちろん、こういったテストを書かざるを得ない状況というのもあると思うのですが、可能な限りユーザーの体験をテストしたいので、ユーザーが知ることができないデータベースの値をテストするのは望ましくないでしょう。実際にユーザーが更新済みの値を見ることができる画面でテストするべきです。

visit edit_user_path(user)
fill_in "名前", with: "新しい名前"
click_button "保存"

expect(page).to have_current_path user_path(user)
expect(page).to have_content "新しい名前"

ボタンクリックなどの操作も同様です。ユーザーは「divタグの3番目の中のボタンをクリックするぞ!」とクリックすることはありませんよね。withinなどと組み合わせて「新着メニューの中にあるボタンをクリックする」というように表現すると、後から見た時に読みやすくなります。

# Bad
all('button', text: "詳細")[2].click

# Good
within_new_menu do
  click_button "詳細"
end

ブラウザのサイズを大きくする

当たり前のことだからしれませんが、あまり言及されている印象がないので書いておきます。ブラウザのサイズは大きければ大きいほどいいです。

Capybara.register_driver(:chrome_headless) do |app|
  options = Selenium::WebDriver::Chrome::Options.new(args: [
    "window-size=3000,3000",
    "headless",
    "disable-gpu",
  ])
  Capybara::Selenium::Driver.new(app, browser: chrome, options: options)
end

ブラウザのサイズが小さい場合、別の要素が被ってくることによって、テストが落ちるなどの問題が起きることがあります。もちろん、ブラウザサイズが小さい画面でテストをしたい状況もあるので、必ずしもこの手が使えるわけではないのですが、特に理由がないならば、ある程度大きく設定しておくことをおすすめします。

簡単にブラウザを起動できる環境を用意する

CIでテストを回すことを考えると、ヘッドレスモードでChromeを動かすことになると思いますが、開発中やテストを書いている段階では、実際にブラウザが立ち上がって操作しているところを見れる方がテンションもあがりますし、問題を特定しやすくなります。

個人的には、環境変数でdriverを簡単に切り替えられるようにしておくと、さっとブラウザを起動してテストが落ちた原因を探ることができるので便利です。

Capybara.configure do
  config.default_driver = ENV['FOREGROUND'] ? :chrome : :chrome_headless
end

おわりに

ブラウザを操作するテストはコストが高く、メンテが難しい、という意見をよく聞きますが、個人的にはコツを抑えて書けば、もっとうまくできるのではないかと思っています。

SmartHRもまだ十分と言える状況ではありませんが、QAチームとも協力しながら、うまくテストを増やしていきたいところです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ruby】エイリアスメソッドの生成/メソッドの削除 /クラスのネスト

エイリアスメソッドの定義

独自に作成したクラスもエイリアスメソッドを定義することができる。

alias 新しいメソッド名 既存メソッド名とすればok

class User
    def hello
        'hello'
    end

    alias greeting hello
end

user = User.new
p user.hello
p user.greeting

 メソッドの削除

undef 削除したいメソッド名とする
objectクラスに定義されたfreezeメソッドを削除する

class User
    undef freeze
end

user = User.new
p user.freeze
#=>
Traceback (most recent call last):
test2.rb:7:in `<main>': undefined method `freeze' for #<User:0x00007fcb77059f70> (NoMethodError)

ネストしたクラスの定義

クラスの内部に定義したクラスは次のようにして参照できる
外側のクラス::内側のクラス

class User
    class BloodType
        attr_reader :type

        def initialize(type)
            @type=type
        end
    end
end

blood_type = User::BloodType.new('B')
p blood_type.type
#=>B

演算子の挙動を独自に再定義する

rubyでは=で終わるメソッドを定義できる
=で終わるメソッドは変数に代入するような形式ででそのメソッドを呼ぶことができる

class User
   #=で終わるメソッドを定義する
   def name=(value)
    @name=value
   end
end
user=User.new
#変数を代入するような形でname=メソッドを呼び出せる
p user.name="Alice"
#=>
"Alice"

==を再定義する

次のようなコードがあったとする
rb:
a=Product.new('A-0001','A GREAT MOVIE')
b=Product.new('B-0001','A AWSOME MOVIE')
c=Product.new('A-0001','A GREAT MOVIE')

同じ商品コードであれば同じ商品であると判別したい
こうなってほしい

a == b #=> false
a == c #=> true

しかしどちらも結果はfalseになります。
なぜならスーパークラスのobjectクラスでは==はobject_idが一致したときにtrueを返す。
なにもしないとこうなる

a==b #=> false
a==c #=> false

本当は次のようになってほしい
rb:
a==b #=> false
a==c #=> true

Productクラスでオーバーライドする

class Product
    attr_reader :code, :name
    def initialize(code, name)
        @code=code
        @name=name
    end

    def ==(other)
        if other.is_a?(Product)
            #商品コードが一致したら同じProductと見直す
            code == other.code
        else
            #otherがProductでなければ常にfalse
            false
        end
    end
end
a=Product.new('A0001','Agreatmovie')
b=Product.new('B0001','Anawesomefilm')
c=Product.new('A0001','Agreatmovie')
p a == b
p a == c
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C++で作るRuby拡張

はじめに

この記事はRuby Advent Calendar 2019の8日目の記事です。

C++でのRuby拡張実装について、つらつらと書いている記事になります。

内容としてはTataraというRubyで型を使えるライブラリを作ってみたで紹介した自作Ruby拡張を作るにあたって得たC++でのRuby拡張実装知見の記事になります。

Ruby拡張って?

皆さんが普段使っているRuby(ここではCRubyのことです)はCによって実装されています。ですので、Cを使ってRubyの拡張機能を作成することもできます。

つまり、Cで既に作成されているライブラリなどをRuby拡張として作成することができるというメリットがあります。CでRuby拡張を実装した場合、Rubyで実装するよりも高速に処理できるケースもあるようです。

実際にCで拡張機能が実装されているgemとしてはsqlite3mysql2などがあります。

またRustやC++でRubyの拡張機能を作成するケースもあります。

例えば最近面白いなぁと思ったのはRustでのRuby拡張を実装できるHelixですね。Rustを使うことでCやC++よりも安全にRuby拡張を書くことができます。

また実装コード自体もかなり読みやすく以下のようなコードでクラスとメソッドを実装できます(※ HelixのREADMEより引用)。

ruby! {
    class Console {
        def log(string: String) {
            println!("LOG: {}", string);
        }
    }
}

ただHelix公式のチュートリアルではRails向けに拡張機能を実装する内容になっています。そのためRuby向けの拡張を作成する際のドキュメントがあまりなく、少し辛いところがあります。

実際にHelixでRuby拡張を作成しているものとしては以下の記事などがあります。

ref: RubyからRustを呼び出すいくつかの方法のまとめ - Qiita

ref: Rustでgemを書く際のハマりどころ in 2017

ref: Writing Ruby gems with Rust and Helix

またC++ではRiceExt++などのRuby拡張を実装できるライブラリも存在しています。

RubyKaigi 2017ではImprove extension API: C++ as better language for extensionにてC++でのRuby拡張実装について紹介されています。

興味のある方はそちらも確認してみると良いでしょう。

今回はC++でのRuby拡張の実装方法について解説します。具体的にはRiceExt++、C++のみでの実装方法などを解説していきます。

つくるもの

今回は、Helloというクラスを作成し、Hello Ruby Extension!と画面に表示するsayというメソッドを実装します。

具体的には以下のようなコードが実行できるRuby拡張を実装していきます。

require 'hello'

Hello.new.say
# => "Hello Ruby Extension!"

今回はRiceExt++、C++でそれぞれ実装していきます。

今回の記事作成にあたって各ライブラリでの実装サンプルをGitHubに上げておきました。興味のある方はこちらも見ると良いかも。

S-H-GAMELINKS/RubyAdventCalendarExtensionSample

実装

Riceでの実装

Riceとは?

Riceとは、C++を使ってRuby拡張を簡単に作成できるライブラリになります。

RiceはgemとしてRubyGemsからインストールすることができます。

gem install rice

これでRiceが使えるようになります!

ちなみに、実際にRiceを使ったサンプルコードは以下のようになります。

#include <iostream>
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

using namespace Rice;

class Hello {
    public:
        Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

extern "C" {
    void Init_hello() {
        Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
            .define_constructor(Constructor<Hello>())
            .define_method("say", &Hello::say);
    }
}

このようにRiceを使う場合、非常に簡単にRuby拡張を作ることができます。

またC++のテンプレートなどを使って以下のようなコードを書くこともできます。

template <class T>
class CppArray {
    public:
        CppArray<T>() {};
};

Data_Type<CppArray<int>> rb_cIntArray = define_class<CppArray<int>>("IntArray")
    .define_constructor(Constructor<CppArray<int>>());

Riceを使うメリットととしては、非常に簡単にC++でのRuby拡張を作ることができる点ですね。C++のライブラリなどをRubyで使えるようにするラッパーなどは、Riceを使って実装するといいかもしれません。

デメリットとしては、日本語のドキュメントもあまりないことと、開発自体があまり活発でない印象があることですね。

日本語で書かれた記事はあまり(Rice以外での実装とかはあったりする)なく、IBMのRice を使用して Ruby の拡張機能を C++ で作成するが日本語で唯一詳しく書かれたRiceのチュートリアルになりそうです、

英語が読める方であれば、こちらのドキュメントを読み解けばよいかと思います。

GitHubのリポジトリでのコミットログなどを見るた印象ではあまり開発が活発な印象はないです。最近、いくつかPull Requestが取り込まれてはいるようですが……。
そのため、Rice側の開発が打ち切られると辛いことになりそうな気配がありますね……。

とはいえ、大きな変更が入る可能性は少ないのでとりあえずC++でのRuby拡張を作る分には良いライブラリだと思います。

実装

それでは、Riceを使ってRuby拡張を実装してみましょう。

なにはともあれ、Riceをインストールしましょう。

gem install rice

インストールが無事終了した後は、extconf.rbというファイルを作成します。これはC++のコードをビルドするMakefileを自動生成するためのファイルになります。CでRuby拡張を作る場合も同様にextconf.rbを作成します。

extconf.rb
require 'mkmf-rice'

create_makefile('hello')

mkmf-riceはRiceを使ってかかれたC++のソースをもとにMakefileを作成するためのライブラリになります。ちなみに、Cで拡張機能を実装する場合はmkmfというライブラリを読み込んでMakefileを自動生成していますね。

またcreate_makefileに渡している文字列がビルドされた拡張ライブラリの名前になります。

次に、hello.cppextconf.rbと同じ階層に作成します。

#include <iostream>
#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

using namespace Rice;

class Hello {
    public:
        Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

extern "C" {
    void Init_hello() {
        Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")
            .define_constructor(Constructor<Hello>())
            .define_method("say", &Hello::say);
    }
}

軽くコードの解説をすると、以下の二行でRiceのヘッダーを読み込んでいます。

#include <rice/Data_Type.hpp>
#include <rice/Constructor.hpp>

RiceではData_Typeを使い、既存のクラスをもとにRuby向けにコンバートしています。

Data_Type<Hello> rb_cHello = define_class<Hello>("Hello")

上記のコードではC++で定義したHelloクラスをRubyで呼び出すHelloというクラスに変換しています。

.define_constructor(Constructor<Hello>())

.define_constructor(Constructor<Hello>())ではC++で定義したHelloクラスのコンストラクタ(Rubyでいうところのinitializeのようなもの)を使って、RubyでHelloクラスのインスタンスを作成できるようにしています。
つまり、Rubyのinitializeを実装しています。

最後に.define_method("say", &Hello::say);sayというメソッドをHelloクラスに追加しています。

.define_method("say", &Hello::say);

これでC++側での実装は完了です。

次に、extconf.rbを実行してMakefileを生成します。

ruby extconf.rb
# => Makefileを自動生成

あとは、makeコマンドでビルドすればhello.ohello.soが生成されていると思います。

make
# => hello.o と hello.so が生成される

最後に、作成したRuby拡張を実際に動かしてみましょう。hello.rbを以下のように作成して実行してみましょう。

hello.rb
require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

Ext++での実装

Ext++とは?

Ext++はRice同様にC++を使って、Ruby拡張を作成できるライブラリです。

Ext++もRubyGemsで配布されているのでgemとしてインストールできます。

gem install extpp

Ext++での実装は以下のようになります。

#include <iostream>
#include <ruby.hpp>

RB_BEGIN_DECLS

void Init_hello() {
    rb::Class klass("Hello");

    klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) {
        return Qnil;
    });

    klass.define_method("say", [](VALUE rb_self) {
        std::cout << "Hello Ruby Extension!" << std::endl;
        return Qnil;
    });
}

RB_END_DECLS

Ext++ではC++のラムダ式を引数に渡して実装することができる点が特徴的です。そのためラムダ式をうまく使うことでRubyのメソッドとC++の実装を一度に書くことができます。

また、Ext++ではruby.hppをインクルードするだけで良いところも便利です。Riceの場合、必要なヘッダーを個別に読み込まなければならず

Riceではラムダ式を使ってメソッドの定義などはできないため、ラムダ式でメソッドを定義したい人はExt++を使うと良いかもしれません

Ext++を使うメリットとしては、実装が一か所で済む点かなと思います。また開発者が日本の方(というか @kou さん)ですので開発者本人にあって話が聴けるという点もメリットかもしれません。

デメリットとしては、サンプルのコードが一つしかなく、人によってはどのように実装を進めていけばいいのかが分かりにくい時がある点でしょうか?その点に関しては今後Pull Requestなどでサンプルコードを投げれたらと思っていますね。
また開発バージョンであり、今後のバージョンアップでは大きな変更も入る可能性もありそうです。

しかしながら、開発者本人に直接話を聞くことができそう(日本人からすると)なので採用するメリットはかなり大きいと思います。

またRiceと違い、Rubyの実装自体に近い実装コードを書くのでCRubyの実装を学んでみたいという人にもオススメかもしれませんね。

実装

それでは、Ext++を使ってRuby拡張を実装していきましょう。

まずはExt++をインストールします。

gem install extpp

インストール完了後、Riceでの実装の時と同じようにextconf.rbを作成します。

extconf.rb
require 'extpp'

create_makefile('hello')

Riceの時とおおよそ同じコードですね。違う点としてはmkmf-riceではなく、extppを読み込んでいます。

次に、hello.cppextconf.rbと同じ階層に作成します。

hello.cpp
#include <iostream>
#include <ruby.hpp>

RB_BEGIN_DECLS

void Init_hello() {
    rb::Class klass("Hello");

    klass.define_method("initialize", [](VALUE rb_self, int argc, VALUE *argv) {
        return Qnil;
    });

    klass.define_method("say", [](VALUE rb_self) {
        std::cout << "Hello Ruby Extension!" << std::endl;
        return Qnil;
    });
}

RB_END_DECLS

Ext++ではrb::Classで新しいクラスを作成します。また、作成したklassdefine_methodを使うことで必要なメソッドを新しく定義しています。

QnilはRubyでのnilを返しています。CRubyのメソッドなどでnilが返ってきているメソッドでは、子のようにreturn Qnil;と書かれています。興味のある方はRuby Hack Challenge Holidayに参加したり、GitHubのruby/rubyのコードを読んでみると良いかもしれません。

あとは。extconf.rbを実行し、Makefileを生成します。

ruby extconf.rb
# => Makefileが生成される

その後、makeで作成したRuby拡張をビルドします。

make
# => hello.o と hello.soが生成される

最後にhello.rbを以下のように作成し、実行してみましょう。

hello.rb
require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

C++での実装

実装

最後にC++でのみで作成するRuby拡張について紹介します。

まずはextconf.rbを作成します。

extconf.rb
require "mkmf"

create_makefile("hello")

mkmfはCRubyに添付されているRuby拡張のためのMakefile作成ライブラリですね。

次に、hello.cppを以下のように作成します。

#include <ruby.h>
#include <iostream>

class Hello {
    public:
        Hello() {};
        ~Hello() {};
        void say() { std::cout << "Hello Ruby Extension!" << std::endl; };
};

static Hello* get_hello(VALUE self) {
    Hello *ptr;
    Data_Get_Struct(self, Hello, ptr);
    return ptr;
}

static void wrap_hello_free(Hello *ptr) {
    ptr->~Hello();
    ruby_xfree(ptr);
}

static VALUE wrap_hello_alloc(VALUE klass) {
    void *ptr = ruby_xmalloc(sizeof(Hello));
    ptr = std::move(new(Hello));
    return Data_Wrap_Struct(klass, NULL, wrap_hello_free, ptr);
}

static VALUE wrap_hello_init(VALUE self) {
    return Qnil;
}

static VALUE wrap_hello_say(VALUE self) {
    get_hello(self)->say();
    return Qnil;
}

extern "C" {
    void Init_hello() {
        VALUE rb_cHello = rb_define_class("Hello", rb_cObject);

        rb_define_alloc_func(rb_cHello, wrap_hello_alloc);
        rb_define_private_method(rb_cHello, "initialize", RUBY_METHOD_FUNC(wrap_hello_init), 0);
        rb_define_method(rb_cHello, "say", RUBY_METHOD_FUNC(wrap_hello_say), 0);
    }
}

ポイントとしては#include <ruby.h>でRuby拡張の実装で使用するマクロや関数などを呼び出している点ですね。これがないとRuby拡張を実装することができません。

またget_hello関数はRubyのインスタンスを引数に受け取って、C++のインスタンスのポインタを返しています。この関数を使うことでC++のクラスのメソッドをラップ関数から呼び出して使うことができるようになります。

wrap_hello_free関数はRubyのGCが呼び出された際にメモリから解放する際の処理がかかれた関数になります。

wrap_hello_allocはインスタンスを作成する際のアロケータになります。wrap_hello_initはRubyでのinitializeになりますね。

あとは、extconf.rbを実行し、makeを実行してビルドしてみましょう

ruby extconf.rb
# => Makfileが生成される

make
# => hello.o と hello.so が生成される

最後に、hello.rbを以下のように作成して実行しましょう。

hello.rb
require './hello.so'

Hello.new.say
ruby hello.rb
# => Hello Ruby Extension!

Hello Ruby Extension!と表示されていればOKです!

おわりに

C++でのRuby拡張についてRice、Ext++、C++それぞれでのでの実装を紹介しました。意外と簡単そうと思っていただければ幸いです。

あと今回の記事ではC++をベースに紹介しましたが、もちろんCでの実装を行う方法もあります。むしろ、そちらのほうが参考になる記事が多いので、Ruby拡張を作る際にはCで作ると良いかもしれません。

あと、この記事でRubyの実装に興味を持たれた方はRuby Hack Challenge Holidayなどに参加してみると良いかもしれません。

意外と簡単にC++でもRubyの拡張機能を作ることができるので、今後もC++の良さげなライブラリなどをRuby向けに実装していきたいと思います。

参考記事

ref: Rice
ref: Ext++
ref: Improve extension API: C++ as better language for extension
ref: Rice を使用して Ruby の拡張機能を C++ で作成する
ref: Rice - Ruby Interface for C++ Extensions
ref: ko1/rubyhackchallenge
ref: ruby/ruby
ref: C++言語で簡単なRuby拡張ライブラリを書いてみた
ref: Rubyの拡張ライブラリの作り方
ref: Rubyソースコード完全解説
ref: TataraというRubyで型を使えるライブラリを作ってみた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails独学でポートフォリオを作成中

こんにちは!現在転職活動中のバスケンです。
初めての投稿です!

railsの独学を初めて3か月が経ちました。
この3か月間の私の学習経過は下記になります。

1、progateでhtml,css,ruby,ruby on rails,githubを1周
2、ruby on rails チュートリアル2週
3、railsでオリジナルのポートフォリオを作成途中

今日は3にて私が作成したポートフォリオの概要を説明します。

ポートフォリオ「KuiShare」の概要

私は人の後悔を聞いたとき似たような後悔が自分に起きないように気をつけるよにします。

このことから、みんなとたくさんの後悔が共有できればおのずと注意深くなり先々で自分に起きうる後悔を減らすことができるのではないかと考えました。

みんなと気軽に後悔を共有することができるのが今回作成した「KuiShare」です。

「KuiShare」のURL「https://kuishare.herokuapp.com/」

「KuiShare」でできること

・後悔したことを投稿/編集/削除
・後悔を共有したいユーザーをフォローする機能
★人の後悔にコメントをする機能
★人の後悔に『ドンマイ』(instagramでいういいね!)をつける機能
・プロフィールの編集機能

利用している技術
・rails
・heroku
・S3(画像置き場)

★印をつけている機能はこれから追加しようとしている機能です。

今回は初投稿でしたのでただ自分の学習経過を記しただけになってしまい申し訳ございません!!
明日からはちゃんとした技術ブログを投稿していきたいと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyでDBMSを実装 字句解析(2日目)

この記事は RubyでDBMS Advent Calendar 2019 の2日目の記事です。

本日の概要

初日は特に中身のないechoサーバとクライアントで終わってしまいましたが、
本日から実際にサーバに渡ってきた文字列をSQLとして解析していきます。
本日はその中でも前半のフェーズである字句解析を実装します。

実装はこちらのGitHubリポジトリに置いてあります。

字句解析とは

字句解析とは、ただの文字列をそれ以上分割できない意味のある最小単位(トークン)に分割する処理です。
例として、以下のSQLを想定した文字列をトークンに分割してみます。

SELECT id,email FROM users WHERE id=5;
文字列 種別
SELECT select keyword -
id string literal id
, comma -
email string literal email
FROM from keyword -
users string literal users
WHERE where keyword -
id string literal id
= equal -
5 numeric literal 5
; semicolon

(種別の部分は筆者の勝手な命名となります)
一部トークン(リテラル)には値も含まれます。
とりあえず、classとして切り出しておきます。

rbdb/query/token.rb
# frozen_string_literal: true

module Rbdb
  module Query
    class Token
      attr_reader :kind, :value
      def initialize(kind, value = nil)
        @kind = kind
        @value = value
      end
    end
  end
end

クエリ設計

では、早速分割処理を実装していきたいところですが、
今後の初期開発でサポートする最低限のクエリを決めておきます。

CREATE文

  • 型はint, varcharのみ。
  • 制約などは含まず。

INSERT文

  • CREATE文でデフォルト値を指定できないので、全カラムを指定するもののみ。

INSERT INTO table_name VALUES (value1, value2);

SELECT文

  • * による全カラム指定と一部カラムのみ指定どちらも可能。
  • WHERE句あり。
  • 条件部分は単純な operand (=|<>|<=|>=|<|>) operand のようなもののみ。

解析処理

では、Lexer(字句解析器という意味)というクラスを作って実装していきます。

rbdb/query/lexer.rb
# frozen_string_literal: true

require 'rbdb/query/token'

module Rbdb
  module Query
    class Lexer
      def initialize(query)
        @query = query
        @sio = StringIO.new(@query)
        @tokens = []
      end

      def scan
        while ch = @sio.read(1) do
          if ch == "'" then
            @tokens << Token.new(:quote)
          elsif ch == '(' then
            @tokens << Token.new(:left_paren)
          elsif ch == ')' then
            @tokens << Token.new(:right_paren)
          elsif ch == '*' then
            @tokens << Token.new(:asterisk)
          elsif ch == ',' then
            @tokens << Token.new(:comma)
          elsif ch == ';' then
            @tokens << Token.new(:semicolon)
          elsif ch == '=' then
            @tokens << Token.new(:equal)
          elsif ch == '<' then
            _next = @sio.read(1)
            if _next == '>' then
              @tokens << Token.new(:not_equal)
            elsif _next == '=' then
              @tokens << Token.new(:less_than_equal)
            else
              back
              @tokens << Token.new(:less_than)
            end
          elsif ch == '>' then
            _next = @sio.read(1)
            if _next == '=' then
              @tokens << Token.new(:greater_than_equal)
            else
              back
              @tokens << Token.new(:greater_than)
            end
          elsif ch =~ /[A-Za-z]/ then
            buf = ch
            while _next = @sio.read(1) do
              if _next =~ /[A-Za-z0-9_]/ then
                buf += _next
              else
                back
                break
              end
            end
            _keyword = keyword(buf)
            if _keyword then
              @tokens << Token.new(_keyword)
            else
              @tokens << Token.new(:string_literal, buf)
            end
          elsif ch =~ /[0-9]/ then
            buf = ch
            has_period = false
            while _next = @sio.read(1) do
              if _next =~ /[0-9\.]/ then
                raise 'tokenize error' if has_period && _next == '.'
                has_period = true if _next == '.'
                buf += _next
              else
                back
                break
              end
            end
            if has_period then
              @tokens << Token.new(:numeric_literal, buf.to_f)
            else
              @tokens << Token.new(:numeric_literal, buf.to_i)
            end
          end
        end
        @tokens
      end

      def back
        @sio.seek(-1, IO::SEEK_CUR)
      end

      def keyword(str)
        case str.upcase
        when "SELECT"
          :select_keyword
        when "FROM"
          :from_keyword
        when "WHERE"
          :where_keyword
        when "INSERT"
          :insert_keyword
        when "INTO"
          :into_keyword
        when "VALUES"
          :values_keyword
        when "CREATE"
          :create_keyword
        when "TABLE"
          :table_keyword
        when "INT"
          :int_keyword
        when "VARCHAR"
          :varchar_keyword
        end
      end
    end
  end
end

大分長いメソッドや、ネストが激しくなってしまいましたが………
StringIO クラスを用いて、先頭から一文字ずつ走査していきます。
'( など一文字目でそのトークン種別を決定できるものもある一方で、
< は、 <><= , < など複数の可能性があるため、
さらに文字の読み込みを進めることで判定します。
また、backというメソッドを用意して、不要な読み込みをしてしまった場合一文字前に戻るようにしています。
アルファベットから始まる文字列は、予約語に当てはまるかをチェックした上で、それ以外を文字列リテラルと解釈します。

動作確認

1日目でそのままechoしていたクエリの代わりに、
分割したTokenクラスをinspectしてそのままレスポンスで返してみます。

rbdb/server.rb
        tokens = Rbdb::Query::Lexer.new(query).scan
        # TODO:
        tokens.each do |token|
          res.body += token.inspect
          res.body += "\n"
        end

クライアントからクエリを叩いてみます。

>> SELECT id,email FROM users WHERE id=5;
#<Rbdb::Query::Token:0x00007f7feb0c5360 @kind=:select_keyword, @value=nil>
#<Rbdb::Query::Token:0x00007f7feb0c51a8 @kind=:string_literal, @value="id">
#<Rbdb::Query::Token:0x00007f7feb0c5130 @kind=:comma, @value=nil>
#<Rbdb::Query::Token:0x00007f7feb0c4bb8 @kind=:string_literal, @value="email">
#<Rbdb::Query::Token:0x00007f7feb0c4578 @kind=:from_keyword, @value=nil>
#<Rbdb::Query::Token:0x00007f7ffb05faa0 @kind=:string_literal, @value="users">
#<Rbdb::Query::Token:0x00007f7ffb05f690 @kind=:where_keyword, @value=nil>
#<Rbdb::Query::Token:0x00007f7ffb05f4d8 @kind=:string_literal, @value="id">
#<Rbdb::Query::Token:0x00007f7ffb05f460 @kind=:equal, @value=nil>
#<Rbdb::Query::Token:0x00007f7ffb05f398 @kind=:numeric_literal, @value=5>
#<Rbdb::Query::Token:0x00007f7ffb05f348 @kind=:semicolon, @value=nil>

大丈夫そうですね!

まとめ

筆者は自作コンパイラなどに手を出したこともないので、
今回初めて字句解析を実装したのですが、なかなか思ってたよりも愚直な処理になりますね。

明日は分割したもののただの配列でしかないTokenたちをSQLの構文として解析していく予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails × CircleCI × ECSのインフラ構築

簡単なDocker RailsアプリをECSを利用して本番環境に上げるまでのまとめ

* あくまで参考に(実務でそのまま利用できるほどしっかり構築しておりません)

image.png

前提知識

ECSとは?クラスターとは?サービスとは?タスクとは?って人は
ECSの概念を理解しよう
などを読んでください。

Railsアプリ作成

まずはローカルでRailsアプリを作成しましょう。
機能は簡単なものでいいので、scaffoldなどを利用してサクッと作成してしまいましょう。
脳死で作成したい人は下記をご覧下さい。
Docker Rails Sampleアプリ構築 - Qiita

AWS上で利用するリソースの作成

コンソール上(or Terraformなど)からあらかじめ作成しておくべきものになります。

IAMロール・ポリシーの作成

ECSで運用するための必要なIAMロール・ポリシーを作成していきます。
ちなみにポリシーとは、ロールに付与される権限情報です。なのでポリシーのないロールは何も権限がない状態なのでまずはポリシーを作成してロールを作成していきましょう。

ポリシーの作成

作成手順

  1. IAMページに行って、サイドバーの「ポリシー」選択
  2. 「ポリシーの作成」ボタン押下
  3. JSONタブを開いて下記に記載したJSON内容をコピペして、「ポリシーの確認」押下
  4. それぞれのポリシー名を入力する

下記の4つのポリシーを作成する。

  1. AmazonSSMReadAccess
  2. AmazonECSTaskExecutionRolePolicy
  3. AmazonEC2ContainerServiceforEC2Role
  4. AmazonECSServiceRolePolicy
AmazonSSMReadAccess
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters",
                "secretsmanager:GetSecretValue",
                "kms:Decrypt"
            ],
            "Resource": "*"
        }
    ]
}
AmazonECSTaskExecutionRolePolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
AmazonEC2ContainerServiceforEC2Role
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeTags",
                "ecs:CreateCluster",
                "ecs:DeregisterContainerInstance",
                "ecs:DiscoverPollEndpoint",
                "ecs:Poll",
                "ecs:RegisterContainerInstance",
                "ecs:StartTelemetrySession",
                "ecs:UpdateContainerInstancesState",
                "ecs:Submit*",
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
AmazonECSServiceRolePolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ECSTaskManagement",
            "Effect": "Allow",
            "Action": [
                "ec2:AttachNetworkInterface",
                "ec2:CreateNetworkInterface",
                "ec2:CreateNetworkInterfacePermission",
                "ec2:DeleteNetworkInterface",
                "ec2:DeleteNetworkInterfacePermission",
                "ec2:Describe*",
                "ec2:DetachNetworkInterface",
                "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:Describe*",
                "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
                "elasticloadbalancing:RegisterTargets",
                "route53:ChangeResourceRecordSets",
                "route53:CreateHealthCheck",
                "route53:DeleteHealthCheck",
                "route53:Get*",
                "route53:List*",
                "route53:UpdateHealthCheck",
                "servicediscovery:DeregisterInstance",
                "servicediscovery:Get*",
                "servicediscovery:List*",
                "servicediscovery:RegisterInstance",
                "servicediscovery:UpdateInstanceCustomHealthStatus"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ECSTagging",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags"
            ],
            "Resource": "arn:aws:ec2:*:*:network-interface/*"
        },
        {
            "Sid": "CWLogGroupManagement",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:DescribeLogGroups",
                "logs:PutRetentionPolicy"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/*"
        },
        {
            "Sid": "CWLogStreamManagement",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:DescribeLogStreams",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/*:log-stream:*"
        }
    ]
}

ロールの作成

IAMページに行って、サイドバーの「ロール」→「ロールの作成」より下記のロールを作成する。
作成後、各ロールのページにて「ポリシーをアタッチする」を押下して上記で作成したポリシーを紐づける。

  1. ecsInstanceRole(→AmazonEC2ContainerServiceforEC2Roleに紐づける)
  2. AWSServiceRoleForECS(→AmazonECSServiceRolePolicyに紐づける)
  3. ecsTaskExecutionRole(→AmazonECSTaskExecutionRolePolicy,AmazonSSMReadAccessを紐づける)

参考
Amazon ECS タスク実行 IAM ロール

ALBの作成

ECSのサービス作成時にALBを登録しておけば、コンテナに動的にポートマッピングをしてくれるようになるので楽になります。

  1. Application Load Balancerを選択
  2. 名前を入力。サブネットを二つ選択。(ない場合は、適宜作成)
  3. セキュリティグループを選択。(ない場合は、適宜作成)
  4. ターゲットグループを選択or作成
  5. ターゲットグループにインスタンスを登録

クラスターの作成

ECSのサイドバーにある「クラスター」から「クラスターの作成」ボタンを押下
「クラスターテンプレートの選択」は「EC2 Linux + ネットワーキング」を選択
1. クラスター名記載
2. EC2インスタンスタイプの選択(お好み)
3. キーペア(お好み。ただし、デバッグ時にSSHできた方がいいので設定しておくことをおすすめ)
4. コンテナインスタンスの IAM ロールに「ecsInstanceRole」を選択

RDSの作成

aws-cliでのRDS作成。
コンソール上からでもOKです。

    aws rds create-db-instance \
            --db-instance-identifier rails-sample-db-production \
            --db-instance-class db.t2.micro \
            --db-subnet-group-name rails-sample-db-subnet-group \
            --engine mysql \
          --engine-version 5.7.26 \
            --allocated-storage 20 \
            --master-username [username] \
            --master-user-password [password] \
            --backup-retention-period 3 \

参考
AWS CLI を使って RDS を作成する (自分用メモ) - Qiita
AWS-CLI Amazon Aurora インスタンス作成 - Qiita

AWS Systems Managerの設定

AWS Systems Managerは、タスク実行時にコンテナに注入する秘匿情報(環境変数)の管理に使えるAWSサービスです。
初めての人は設定の仕方を含め、
ECSでごっつ簡単に機密情報を環境変数に展開できるようになりました!
を見れば大体分かると思います。

AWS Systems Managerの左側メニューから「パラメータストア」→「パラメータの作成」をクリック。パラメータの詳細画面が表示されるので、パラメータのキー名と値を入力します。タイプには「安全な文字列」を選択します。

パラメータのキー名と値一覧

キー名
/production/database_username [RDSに設定したusername]
/production/database_password [RDSに設定したpassword]
/production/database_host [RDSインスタンスのエンドポイント]

RDSインスタンスのエンドポイント(RDS→データベース→[インスタンス名])
image.png

CircleCIの設定

circleci/config.yml
version: 2.1
orbs:
  aws-cli: circleci/aws-cli@0.1.13
executors:
  builder:
    docker:
      - image: circleci/buildpack-deps

commands:
  init:
    steps:
      - checkout
      - aws-cli/install
      - install_ecs-cli
      - setup_remote_docker
  install_ecs-cli:
    steps:
      - run:
          name: Install ECS-CLI
          command: |
            sudo curl -o /usr/local/bin/ecs-cli https://amazon-ecs-cli.s3.amazonaws.com/ecs-cli-linux-amd64-latest
            sudo chmod +x /usr/local/bin/ecs-cli

jobs:
  build:
    executor: builder
    steps:
      - init
      - run:
          name: Build application Docker image
          command: |
            docker build -f build.Dockerfile --rm=false -t rails-sample-app-build:latest .
      - run:
          name: Save image
          command: |
            mkdir -p /tmp/docker
            docker save rails-sample-app-build:latest -o /tmp/docker/image
      - persist_to_workspace:
          root: /tmp/docker
          paths:
            - image
  deploy:
    executor: builder
    steps:
      - init
      - attach_workspace:
          at: /tmp/docker
      - run: docker load -i /tmp/docker/image
      - run:
          name: Assets precompile and Push Docker image
          command: |
            docker build -f assets.Dockerfile --build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY} --rm=false -t rails-sample-app-build:latest .
      - run:
          name: Push Docker image
          command: |
            ecs-cli push rails-sample-app-build:latest
      - run:
          name: ECS Config
          command: |
            ecs-cli configure \
            --cluster rails-sample-${CIRCLE_BRANCH} \
            --region ${AWS_DEFAULT_REGION} \
            --config-name rails-sample-${CIRCLE_BRANCH}
      - run:
          name: migrate deploy
          command: |
            ecs-cli compose \
            --file ecs/${CIRCLE_BRANCH}/migrate/docker-compose.yml \
            --ecs-params ecs/${CIRCLE_BRANCH}/migrate/ecs-params.yml \
            --project-name rails-sample-${CIRCLE_BRANCH}-migrate \
            up \
            --launch-type EC2 \
            --create-log-groups \
            --cluster-config rails-sample-${CIRCLE_BRANCH}
      - run:
          name: Unicorn + Nginx deploy
          command: |
            ecs-cli compose \
            --file ecs/${CIRCLE_BRANCH}/app/docker-compose.yml \
            --ecs-params ecs/${CIRCLE_BRANCH}/app/ecs-params.yml \
            --project-name rails-sample-${CIRCLE_BRANCH}-app \
            service up \
            --container-name nginx \
            --container-port 80 \
            --target-group-arn ${TARGET_GROUP_ARN} \
            --timeout 0 \
            --launch-type EC2 \
            --create-log-groups \
            --cluster-config rails-sample-${CIRCLE_BRANCH}

workflows:
  version: 2
  build-deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - master

CircleCIに設定する環境変数

CircleCIのプロジェクトの設定ページ(Settings→[アカウント名or組織名]→[プロジェクト名])に行き、下記の画像の箇所から設定する
https://circleci.com/gh/[アカウント名or組織名]/[プロジェクト名]/edit#env-vars

image.png

環境変数名
AWS_ACCESS_KEY_ID [AWSのアクセスキーID]
AWS_ACCOUNT_ID [AWSのアカウントID]
AWS_DEFAULT_REGION [AWSのデフォルトリージョン]
AWS_ECR_REPOSITORY_URL [AWSのECRリポジトリURL]
AWS_SECRET_ACCESS_KEY [AWSのシークレットアクセスキー]
RAILS_MASTER_KEY [config/master.keyの値]
TARGET_GROUP_ARN [ターゲットグループのarn]

Task definitionの作成

docker-compose.yml

rails-sample/ecs/production/app/docker-compose.yml
version: "3"

services:
  app:
    image: [ECRのリポジトリURI]
    entrypoint: bundle exec unicorn -c config/unicorn.rb
    env_file:
      - ../env
    working_dir: /projects/rails-sample
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "rails-sample-production/app"
        awslogs-stream-prefix: "rails-sample-app"
  nginx:
    image: [ECRのリポジトリURI]
    ports:
      - 0:80
    links:
      - "app:app"
    env_file:
      - ../env
    working_dir: /projects/rails-sample
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "rails-sample-production/nginx"
        awslogs-stream-prefix: "rails-sample-nginx"

* Nginxの設定ファイルは適宜用意してください。上記のnginxの欄にnginx設定ファイル群の設置・起動用のスクリプトentrypoint: /bin/bash /etc/nginx/start.shを用意するなど。

ecs-params.yml

タスク実行時に実行ロールの指定やコンテナに注入する環境変数をAWS Systems Managerから取得するして設定するためのファイル

rails-sample/ecs/production/app/ecs-params.yml
version: 1
task_definition:
  # タスク実行時のロールを指定
  task_execution_role: ecsTaskExecutionRole
  services:
    # 起動するコンテナを記載(app, nginx)
    app:
      # 何らかの理由で失敗・停止した際に、タスクに含まれる他のすべてのコンテナを停止するかどうか(デフォルトはtrue)
      essential: true
      # AWS Systems Managerから秘匿情報を取得してコンテナに環境変数を注入
      secrets:
        - value_from: /production/database_username
          name: DATABASE_USERNAME
        - value_from: /production/database_password
          name: DATABASE_PASSWORD
        - value_from: /production/database_host
          name: DATABASE_HOST
    nginx:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      assign_public_ip: ENABLED

コンテナ全体に注入する環境変数の設定

各環境(production, stagingなど)ごとのディレクトリ以下にenvファイルを用意してそこに記載する

# ここのファイルに追加した環境変数は全てのコンテナに展開されます
# Rails
APP_HOST=54.238.241.230
RAILS_ENV=production
RAILS_LOG_TO_STDOUT=1
RAILS_SERVE_STATIC_FILES=1

# RDS
DATABASE_NAME=rails-sample_production
DATABASE_PORT=3306
DATABASE_POOL=10

# Unicorn
UNICORN_PORT=23380
UNICORN_TIMEOUT=180
UNICORN_WORKER_PROSESSES=2

# Nginx専用
NGINX_APP_SERVER_NAME=app
NGINX_APP_SERVER_PORT=23380
NGINX_DOCUMENT_ROOT=/projects/rails-sample/public
NGINX_FRONT_SERVER_NAME=54.238.241.230

構築の際に詰まる可能性のあるポイント

ECSコンテナインスタンスの作成

Defaultクラスター作成しているし、IAMロールにecs:CreateClusterの権限付与されているから自動で作成なんかもしてくれるのかと思ったら作成してくれなかった。
なので、クラスター作成→インスタンス作成の方が良い(ちな、クラスター作成時にインスタンスも作成するようにはできるっぽい)
→カスタマイズされてるAMI利用時のみ初期スクリプトによってDefaultクラスターを作成しているのかもしれない

:hatched_chick: 参考 :hatched_chick:
Amazon ECS コンテナインスタンスの起動 - Amazon Elastic Container Service
Amazon ECS-optimized AMI - Amazon Elastic Container Service

インスタンスタイプについて

image.png

ある程度余裕持たないとタスク実行するための容量を持たなくて死ぬ
(ほんとは、ローカルや本番環境で動かした時の使用量見てタスク実行に必要なメモリを設定した方が良い)

ecs-cliでのタスク実行

  • ecs-params.yml ファイル内でtask_execution_roleを指定すること
  • task_execution_roleで指定した適切なポリシーを適用したIAM Roleを用意すること(エラーが出なくて、単純に実行されないので気づきにくい)

まとめ

ECSについてググればたくさん記事出てくるのですが、実際に活用しようとしてみるとたくさん落とし穴があります。もし利用しようか考えている人は一度デモアプリで利用してみることをお勧めします。

最後に

UUUMではインフラに詳しいエンジニアを欲しています。
詳しくはこちら →→→→→→ UUUM攻殻機動隊の紹介

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker Rails Sampleアプリ構築

適当なRailsアプリを作成するのに脳死で作成する

前提

  • Ruby 2.6.5
  • Railsバージョン6.0.1
  • MySQL 5.7
  • Node.js 8系
  • webpacker用のコンテナは用意していない
$ mkdir rails-sample

$ rbenv local [使用するrubyバージョン]

$ git init

$ bundle init
gem 'rails'のコメントアウトを外す

$ bundle install --path vendor/bundle

$ bundle exec rails new . -B -d mysql --skip-test
-B bundle install をスキップする(お好み)
-d 利用するDBを指定(デフォルトはSQLite)
--skip-test railsのデフォルトのminitestというテストを利用しない場合は指定(お好み)

Gemfileの上書きしていいかどうかは Y でEnter

$ bundle exec rails webpacker:install

.gitignore に vendor/bundleを追記(お好み)

docker-compose.ymlとDockerfile作成

Dockerfile

FROM ruby:2.6.5
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs

# nodejsとyarnはwebpackをインストールする際に必要
# Node.js
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
apt-get install nodejs
# yarnパッケージ管理ツール
RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn

WORKDIR /tmp
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install
ENV APP_HOME /rails-sample
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
COPY . /rails-sample
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - ./tmp/docker/mysql:/var/lib/mysql:delegated

  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/chiko
    ports:
      - "3000:3000"
    depends_on:
      - db

database.ymlを編集(お好み)

database.yml
default: &default
  adapter: mysql2
  timeout: 5000
  encoding: utf8mb4
  charset: utf8mb4
  collation: utf8mb4_general_ci
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: db
  port: 3306

development:
  <<: *default
  database: rails-sample_development

test:
  <<: *default
  database: rails-sample_test

production:
  <<: *default
  database: <%= ENV["DATABASE_NAME"] %>
  username: <%= ENV["DATABASE_USERNAME"] %>
  password: <%= ENV["DATABASE_PASSWORD"] %>
  host: <%= ENV["DATABASE_HOST"] %>
  port: <%= ENV["DATABASE_PORT"] %>

defaultに
- charset: utf8mb4
- collation: utf8mb4_general_ci
- port: 3306

を追記

productionは
- database: <%= ENV["DATABASE_NAME"] %>
- username: <%= ENV["DATABASE_USERNAME"] %>
- password: <%= ENV["DATABASE_PASSWORD"] %>
- host: <%= ENV["DATABASE_HOST"] %>
- port: <%= ENV["DATABASE_PORT"] %>

を全部環境変数に変更

$ docker-compose build

$ docker-compose run --rm web rails db:create

ScaffoldでUserモデル作成

$ docker-compose run --rm web rails g scaffold user name:string age:integer

トップページを用意

$ bundle exec rails g home index
routes.rb
Rails.application.routes.draw do
  root 'home#index' # これを追記

  resources :users
end

Userページへのリンクを付与

index.html.erb
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

<%= link_to "user", users_path %>   <%# これを追記

マイグレーションして、コンテナを立ち上げる

$ docker-compose run --rm web rails db:migrate

$ docker-compose up -d

=> http://localhost:3000 にアクセスして確認

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

つくってまなぶ★DES☆

この記事は信州大学kstmアドベントカレンダー2019の三日目の記事です。
去年と同様に 人不足のため わたくし @arsley の1/2回目の記事となります。よろしくお願いします。

:pencil: 導入

本記事では私がゼミにて勉強した DES (Data Encryption Standard) とかいう「暗号化方式の1手法として聞いたことはあるけど、どういうものか知らない」という技術について、 得た知識をすぐに他者に伝えマウントをとってしまう若き頃のような気持ちで 実装例を挙げながら説明します。
これを機に暗号分野にホーーーーンの少しでも興味を持っていただければ幸いです:pray:

:tired_face: TL;DWR

too long don't wanna read
完成品はこちら(3分間クッキング)(Ruby実装)(雑README)

「手っ取り早くどういう挙動をするものなのか知りたい」という方はご参照ください。

:lock: DES (Data Encryption Standard)

DES (Data Encryption Standard) は共通鍵暗号方式のブロック暗号の一つです。
64ビットをブロック長、鍵も同じく64ビットで与え一連の暗号化処理により暗号文を得ます。
ただし、鍵については64ビットのうち8ビットは誤り訂正ビット(パリティビット)として扱うため、実際の鍵長は56ビットとなります。
また、暗号化に用いた 共通の 鍵を用いて暗号文の復号を行うことが可能です。

共通鍵暗号は字面でわかると思うので、ブロック暗号について少しだけ補足します。

:question: ブロック暗号

ブロック暗号とは、その名の通りデータを固定長の ブロック という単位に区切り、ブロックごとに暗号化を行う方式のことを指します。
共通鍵暗号におけるもう一つの暗号化方式はストリーム暗号と呼ばれるもので、1ビットもしくは1バイト単位で逐次暗号化していく方式のことを指します。

一般的にブロック暗号は、ラウンド関数と呼ばれる処理を繰り返し適用し暗号文を得る構造となっており、これをFeistel構造と言います。
DESの開発者であるHorst Feistelに由来するそうです1
また、暗号には平文→暗号文といった暗号化のほかに、暗号文→平文といった復号が可能である必要がある(復号可能性)のですが、このFeistel構造は逆変換が自身と同じ形になることから復号可能性を保証できるそうです。不思議ですね
もちろんDESもこのFeistel構造により実装されています。

ちょっとわかりにくいので「逆変換が自身と同じ形になる」ということを線形代数の一次変換でお話をすると、たとえば点 $(x,y)$ から $(x^\prime, y^\prime)$ への一次変換を

A =
\left(
\begin{matrix}
  1 & 0 \\
  0 & 1
\end{matrix}
\right)
\left(
\begin{matrix}
  x^\prime \\
  y^\prime
\end{matrix}
\right)
=
A
\left(
\begin{matrix}
  x \\
  y
\end{matrix}
\right)

で与えると、これの逆変換すなわち$A^{-1}$も同じであり、これが「逆変換が自身と同じ形になる」ということの意です(多分)。

:bulb: DESのDEA (Data Encryption Algorithm)

以降、転置表などのデータはこちらのサイトのものを利用させていただいています。

DESにおいて暗号文を得るまでのプロセスをDEA(Data Encryption Algorithm)と呼ぶこともあるそうです初耳でした

上述したように、DESはラウンド関数を繰り返し適用するFeistel構造により構成されます。
64ビット長のデータをブロックとして扱い、同じく64ビットを鍵として扱います(実際に使う鍵長は56ビット)。

DESは大きく分けて次の手順で暗号化を行います。

  1. 元々の鍵64ビットから転置・シフト演算を用いて16個のサブ鍵を取得
  2. ブロックの初期データに対し初期転置を適用
  3. (2)にて得られたデータに対しサブ鍵と共にラウンド関数へ適用
    • これを16回繰り返す
  4. (3)にて得られたデータに対して最終転置を適用、これを暗号文とする

図にするとこんな感じです。

des.png

:question: 転置

中身を明らかにする前に、転置という操作について説明します。
例えば8文字の文字列 abcdefgh に対して次のような数列

8 7 6 5 4 3 2 1

が与えられた時、これは元の文字列を hgfedcba に変換することを意味します。
具体的にいうと、「元の文字列における8番目を1文字目に、元の文字列における7番目を2文字目に、元の文字列における6番目を3文字目に...置き直す(転置する)」という操作を行わせることを意味します。

:lock: サブ鍵生成

まずサブ鍵生成について説明します。
今回鍵としては文字列 kkkeeyyy の2進表記 $0110101101101011011010110110010101100101011110010111100101111001$ を利用します。

以下は文字列→二進表記変換の例です2

key_bin = key.bytes.map { |k| k.to_s(2).rjust(8, '0') }.join

転置1 PC-1

まずはじめに最初の転置 PC-1 を行います。
この転置PC-1は次の数列により表されます3

PC-1
57 49 41 33 25 17 9 1 58 50 42 34 26 18 10 2 59 51 43 35 27 19 11 3 60 52 44 36 63 55 47 39 31 23 15 7 62 54 46 38 30 22 14 6 61 53 45 37 29 21 13 5 28 20 12 4

転置を行い得られたビット列は $00000000111111111111111111100000011100011000111001110000$ となります(めっちゃ整っててびっくり)。
この転置から使われているのは 8,16,24,32,40,48,56,64 を除く56ビットであり、鍵長が与えたデータよりも短いことがわかるかと思います。

転置処理には map を用いるのが楽かなと思います4

PC1.map { |index| key_bin.chars[index] }

シフト演算

シフト演算はその名の通り、与えられたビット列を左方向へシフトさせるものです。
ここで用いるシフト演算は循環シフトで、上位方向へ溢れた桁は一番下位の桁へと戻るものとなります。

シフト演算の適用に際して、先ほどの転置で得られたビット列を28ビットで半分に分割し、これを

C_0 = 0000000011111111111111111110 \\
D_0 = 0000011100011000111001110000

とおくこととします。
そしてこのシフト演算を 16回 適用するのですが、「何回目のシフト演算か」によりシフト量が異なります。
シフト量は以下の表の通りです。

何回目? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
シフト量 1 1 2 2 2 2 2 2 1 2 2 2 2 2 2 1

1度目のシフト演算により

C_1 = 0000000111111111111111111100\\
D_1 = 0000111000110001110011100000

が得られ、この操作を$C_{16}, D_{16}$が求まるまで繰り返します。

コード例については次の説明で示します。

転置2 PC-2

前述したシフト演算により得られる$C_{1\dots 16}, D_{1\dots 16}$それぞれについて、各々を結合した $C_nD_n$に対して以下の転置を適用することで 16個 のサブ鍵 $K_{1\dots 16}$ を得ます。

PC-2
14 17 11 24 1 5 3 28 15 6 21 10 23 19 12 4 26 8 16 7 27 20 13 2 41 52 31 37 47 55 30 40 51 45 33 48 44 49 39 56 34 53 46 42 50 36 29 32

例として一つ目のサブ鍵 $K_1$ は $C_1D_1 = 00000001111111111111111111000000111000110001110011100000$ より $K_1 = 111100001011111011100110000000011110111010101000$ となります。

各シフトにより得られる$C_n,D_n$を利用するので、シフト演算の繰り返し操作と同時に行うといいと思います5

SHIFT.each do |s|
  c << (c.last[s..-1] + c.last[0...s])
  d << (d.last[s..-1] + d.last[0...s])
  @keys << permutate_with_pc2(c.last + d.last)
end

:lock: 暗号化

サブ鍵を生成したらいよいよ暗号化のプロセスに入ります。
今回は暗号化したい文字列として ddaattaa およびその二進表記 $0110010001100100011000010110000101110100011101000110000101100001$ を用います。

初期転置 IP

入力として与えられたブロックに対し以下の転置を適用します。

IP
58 50 42 34 26 18 10 2 60 52 44 36 28 20 12 4 62 54 46 38 30 22 14 6 64 56 48 40 32 24 16 8 57 49 41 33 25 17 09 1 59 51 43 35 27 19 11 3 61 53 45 37 29 21 13 5 63 55 47 39 31 23 15 7

ddaattaa の二進表記に対して適用すると $1111111100110000001100111100110000000000111111110000000000000000$ となります。
得られた転置後のデータを半分(32ビット)で分割し

L_0 = 11111111001100000011001111001100\\
R_0 =00000000111111110000000000000000

とおきます。

コード例はこんな感じです6

def permutate_with_ip
  IP.map { |index| message_bin.chars[index] }
end

:exclamation: ラウンド関数

それではDESの要となるラウンド関数について説明していきます。
ラウンド関数を $F$ としたとき、DESにおける処理プロセスは以下の式で表されます($n = 0\dots 15$、$\oplus$ はXORの意)。

\begin{aligned}
L_{n+1} &= R_n \\
R_{n+1} &= L_n \oplus F(R_n, K_{n+1})
\end{aligned}

$n$ の範囲からわかるようにラウンド関数は 16回 適用します。おそろしい。
図で表すとこのような形です。

round.png

このラウンド関数には次の処理が含まれています。

  1. 拡大転置Eを$R_n$に対して適用 (32ビット→48ビット)
  2. 対応するサブ鍵 $K_{n+1}$ と(1)の結果とのXORをとる (48ビット→48ビット)
  3. (2)の結果に対してSボックスを適用 (48ビット→32ビット)
  4. (3)の結果を結合したものに対して転置Pを適用 (32ビット→32ビット)

(3)のSボックスの説明は後に譲ることとして、拡大転置E・転置Pおよびサブ鍵とのXORの説明を簡単にしておきます。

拡大転置Eと転置P

拡大転置Eと転置Pは以下のように表されます。

E,P
# E
32 1 2 3 4 5 4 5 6 7 8 9 8 9 10 11 12 13 12 13 14 15 16 17 16 17 18 19 20 21 20 21 22 23 24 25 24 25 26 27 28 29 28 29 30 31 32 1

# P
16 7 20 21 29 12 28 17 1 15 23 26 5 18 31 10 2 8 24 14 32 27 3 9 19 13 30 6 22 11 4 25

コード例も今までの転置とさほど変わらないですね7 8

# E
def permutate_with_e(r)
  E.map { |index| r[index] }
end

# P
def permutate_with_p(transposed_r)
  P.map { |index| transposed_r[index] }
end

またサブ鍵とのXORについてですが、RubyにおけるXORは他言語と同じく (?) ^ で表現できるため以下のように書きました9

def xor_with_key(permutated_r, key_index)
  r_xor_key = []
  permutated_r
    .zip(keys[key_index])
    .each { |right, key| r_xor_key << (right.to_i ^ key.to_i).to_s }
  r_xor_key
end

あんまりいい書き方ではなさそうですね...

:books: Sボックス

Sボックスも今までに示した転置と同様に「与えられた入力を一定の法則に基づいて並べ換える」操作であることには変わりありません。
ただし単純な転置と異なり、表の中から対応する数値を一つ選択しそれの2進数表記を返すというものとなっています。

この操作の前に、ddaattaa に対し拡大転置Eを適用しサブ鍵 $K_1 = 111100001011111011100110000000011110111010101000$ とのXORをとった結果を $I = 111100001010100100011000100000011110111010101000$ とおきます(用意しておきます)。
この $I$ (48ビット)をまず6ビットごと8つに分割します。

I = 111100\quad 001010\quad 100100\quad 011000\quad 100000\quad 011110\quad 111010\quad 101000

簡単のためそれぞれのビットの塊を $i_n$ としてまとめ

I = i_1 i_2 i_3 i_4 i_5 i_6 i_7 i_8

と表すこととします。
この各々の $i_1 \dots i_8$ 対して $S_1 \dots S_8$ というSボックスをそれぞれ適用します。
具体的には

S_1(i_1)S_2(i_2)S_3(i_3)S_4(i_4)S_5(i_5)S_6(i_6)S_7(i_7)S_8(i_8)

ということです。
お気づきかもしれませんが、このSボックスは 8つ あります。
解説のために $S1$ のみ示します。

$S1$ row\col 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 14 4 13 1 2 15 11 8 3 10 6 12 5 9 0 7
1 0 15 7 4 14 2 13 1 10 6 12 11 9 5 3 8
2 4 1 14 8 13 6 2 11 15 12 9 7 3 10 5 0
3 15 12 8 2 4 9 1 7 5 11 3 14 10 0 6 13

Sボックスは入力ビット列における 最初と最後 を合わせたビット列を 行番号とし、残った 中間の4ビット列列番号 とし、対応する数値の 4ビット二進表記 を返す関数のような働きをします。
入力ビット列を $b_1 b_2 b_3 b_4 b_5 b_6$ と表すならば、$(b_1 b_6)_2$ 行 $(b_2 b_3 b_4 b_5)_2$ 列に相当する数値 $y$ の2進表記を返します。

$S1$ へ適用する $i_1 = 111100$ を例にとって説明すると、$10_2$ 行 $1110_2$ 列目すなわち2行14列目に相当する $5_{10}$ の2進表記 $0101_2$ を返します。
この操作を全ての $i_n$ に適用すると結果として

S_1(i_1)S_2(i_2)S_3(i_3)S_4(i_4)S_5(i_5)S_6(i_6)S_7(i_7)S_8(i_8) = 01011011010010110100101101011001

が得られます。

8つあるSボックスはここを参考にしてもらうとして、コード例はこんな感じです9

def transpose_with_s_table(xored_r)
  transposed_r = []
  xored_r.each_slice(6).with_index do |bits, i|
    x = bits[1..4].join.to_i(2)
    y = (bits[0] + bits[-1]).to_i(2)
    transposed_r += DES::S[i][y][x].to_s(2).rjust(4, '0').split('')
  end
  transposed_r
end

最終転置 IPinverse :arrow_right: 暗号文

暗号文を得る最後のプロセスとなる最終転置について説明します。

ラウンド関数 $F$ を複数回適用して $L_{16}$ および $R_{16}$ を得ることができました(唐突)。

L_{16} = 10000000100010100101010011100110 \\
R_{16} = 01010101111011001100101000000010

この2つを 左右を逆にして結合 します。
すなわち

R_{16} L_{16} =0101010111101100110010100000001010000000100010100101010011100110

となります。
これに対して下記の最終転置 $IP^{-1}$ を適用します。

ipinverse
40 8 48 16 56 24 64 32 39 7 47 15 55 23 63 31 38 6 46 14 54 22 62 30 37 5 45 13 53 21 61 29 36 4 44 12 52 20 60 28 35 3 43 11 51 19 59 27 34 2 42 10 50 18 58 26 33 1 41 9 49 17 57 25

はい。これにて ddaattaakkkeeyyy にて暗号化した暗号文

Encrypted = 0100000000100111010110100011010001001000000100100101111010110110

が得られました:tada:
無理やり文字化すると @'Z4H\x12^\xB6 だそうです。読めませんね。

:unlock: 復号?

最後に復号について話します。

ブロック暗号の説明でFeistel構造のはなしをしました。
Feistel構造は 逆変換が自身と同じになる という性質がありました。
そのため、 暗号化のプロセスを全て逆に行うことで平文が得られる ということになります。

結論をいうと、暗号文に対してサブ鍵を $K_1, K_2 \dots K_{15}, K_{16}$ のような昇順ではなく、 $K_{16}, K_{15} \dots K_2, K_1$ のように降順にして適用することで平文が得られます。
不思議ですね...

:zap: おしまい

あんまり「つくってまなぶ」要素がなくなってしまいましたね、残念。
まあでも数多あるプログラミング言語はどれも「目的を達成するためのツール」でしかないと思っているので、実装したいものを理解し、実装方針を立てれば自ずと作れるはずですよね...?
今回お話しした「DESをつくる」というものは車輪の再発明になってしまうものの典型ですが、これを機に「他の暗号方式はどんな実装になっているんだろう...?」というような興味をもつ一歩になってくれれば嬉しいですね:smile:
RSA暗号とか楕円曲線暗号とかになると多少の数学要素が混じってくるので厳しいものもあるとは思いますが...

あ、Gistを見てくださった方はわかると思うのですが、参考にさせていただいたサイトの数値を使ってテストも書いてあります、TDDで作ってました。
欲しい答えとか欲しい結果が明確な場合にはTDDはさいっこうにキマりますねえ、皆さんもTDDキメていきませんか?

:books: 参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby | Rails】Dockerfileの中で"ADD Gemfile ~ RUN bundle install"をするのはやめませんかという話

今回の検証環境

  • Ruby 2.6.5
  • Docker 19.03.5
  • Docker-Compose 1.24.1

はじめに

  • Railsの設定を例にすると結構複雑になっちゃうので、今回は単純にRubyをDocker上で使用する例で解説します。
    Railsを使用する場合も要点は同じなので適宜読み替えてください。
  • 今回はhogehogeディレクトリ配下で作業します。サンプルコード中のhogehogeの部分は自由に変更して構いません。

やめませんか

RailsやRubyのDocker環境構築の解説をしている記事で、Dockerfile内で以下のようにADD Gemfile~RUN bundle installしている記事を本当にたくさんよく見かけます。

Dockerfile
FROM ruby:2.6.5

WORKDIR /hogehoge

RUN gem install bundler

# ↓こういうの↓
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install

これ、やめませんか?

なんでやねん

Dockerfileの中でADD Gemfile~RUN bundle installをすることには以下のようなデメリットがあります。

  • Gemfileを編集するたびに毎回docker builddocker-compose buildをしないといけなくなる。
    • Gemを1つ追加するだけでも全Gemをインストールし直す羽目になる。
      • nokogiriとかインストール遅いよね!毎回待たされるの嫌だよね!

何よりRubyistの皆さんとしては、Gemfileを編集したら本能的にbundle installしたいですよね?したくないですか?したいですよね?

じゃあどうすんねん

docker-composeを上手く使えばもっと効率よく楽しく開発できます。

Dockerfile

containersディレクトリ配下にDockerfileを作成します。

containers/Dockerfile
FROM ruby:2.6.5

WORKDIR /hogehoge

RUN gem install bundler

こんだけ。

docker-compose.yml

次に、アプリケーションのルートディレクトリにdocker-compose.ymlを作成します。

docker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
      dockerfile: containers/Dockerfile
    environment:
      # これがないとGemを`vendor/bundle`以下から読み込んでくれないので注意
      # (正確には、`.bundle/config`の設定を読み込んでくれない)
      BUNDLE_APP_CONFIG: /hogehoge/.bundle
    volumes:
      - .:/hogehoge

こんだけ。
単純にカレントディレクトリ全体をマウントしてるだけですね。

ビルドしよう

さぁdocker-compose buildしていきましょう。

$ docker-compose build
Building app
Step 1/4 : FROM ruby:2.6.5
 ---> d98e4013532b
Step 2/4 : ENV APP_ROOT /hogehoge
 ---> Using cache
 ---> 97b5a8bca2d0
Step 3/4 : WORKDIR $APP_ROOT
 ---> Using cache
 ---> 54066d2ae384
Step 4/4 : RUN gem install bundler
 ---> Using cache
 ---> 290d99a58c5b
Successfully built 290d99a58c5b
Successfully tagged hogehoge_app:latest

すぐ終わりますね。
(初回だけruby:2.6.5のDocker imageのpullに時間がかかります。)

Gemをインストールしてみよう

とりあえずGemfileを作成します。

$ docker-compose run --rm app bundle init
Writing new Gemfile to /hogehoge/Gemfile

適当にbcryptでも入れてみますかね。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"

# ↓追加↓
gem 'bcrypt'

それでは念願のbundle installです。
docker-compose経由で実行するのと、--pathオプションを指定するのを忘れずに。

補足: 一度--pathオプションを付けてbundle installを実行すると、.bundle/configが作成されて設定が追加されるため、次回以降bundle installの際に--pathオプションを付ける必要はありません。

$ docker-compose run --rm app bundle install --path vendor/bundle
Creating network "hogehoge_default" with the default driver
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Fetching bcrypt 3.1.13
Installing bcrypt 3.1.13 with native extensions
Using bundler 2.0.2
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Bundled gems are installed into `./vendor/bundle`

ちゃんとインストールできているか確認してみましょう。

$ docker-compose run --rm app bundle exec gem list

*** LOCAL GEMS ***

bcrypt (3.1.13)
bundler (2.0.2)

できていますね。
次回以降もGemfileを編集した際にはdocker-compose run --rm app bundle installするだけで大丈夫です。
docker-compose buildし直す必要はありません。

試しにRubyスクリプトを実行してみよう

test.rbを作って適当にbcryptを使ってみます。

test.rb
# vendor/bundle配下から読み込むようにしてくれる
require 'bundler/setup'

# Gemfileの中のGemを一発でrequireしてくれる
Bundler.require

# NOTE: ↑上の2つはRailsの場合は勝手にやってくれるため必要ないです↑

puts BCrypt::Password.create('password')
$ docker-compose run --rm app ruby test.rb
$2a$12$xWXitLplfvcIuxUdTg.1I.bb/Jo0btGGnqWE02ZiMFsne.hDQXaDW

実行できましたね。

1つ問題点が!!

現状だとインストールしたGemはローカルvendor/bundleディレクトリ配下に配置されます。
docker-compose run ...を実行するたびにこのvendor/bundleディレクトリ配下が毎回マウントされるため、
Gemが増えてくるとdocker-compose run ...を実行するたびにマウントに時間がかかり、コマンド実行が遅くなってしまいます。(この問題はDocker for Macを使用している場合のみ発生するらしいです)

「毎回docker-compose buildし直すのが面倒だからこうしたのに、本末転倒じゃねぇか!!」

落ち着いてください。こんな時のためにDockerにはvolumeという機能があるじゃないですか。

vendor/bundleをボリュームに切り出す

docker-compose.ymlを以下のように修正するだけで解決します。

docker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
      dockerfile: containers/Dockerfile
    environment:
      BUNDLE_APP_CONFIG: /hogehoge/.bundle # これがないとGemを`vendor/bundle`以下から読み込んでくれないので注意
    volumes:
      - .:/hogehoge
      # ↓追加↓
      - bundle:/hogehoge/vendor/bundle

# ↓追加↓
volumes:
  bundle:
    driver: local

ローカルの方のvendor/bundle配下のファイルはもう必要ないため削除しちゃいましょう。

$ rm -rf vendor/bundle/*

ボリュームとして切り出したら改めてbundle installし直しましょう。

$ docker-compose run --rm app bundle install
Creating volume "hogehoge_bundle" with local driver
Fetching gem metadata from https://rubygems.org/.
Fetching bcrypt 3.1.13
Installing bcrypt 3.1.13 with native extensions
Using bundler 2.0.2
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Bundled gems are installed into `./vendor/bundle`

これでvendor/bundleディレクトリ配下はbundleボリュームとして切り出されて毎回ローカルからマウントされることがなくなるため、docker-compose runで余計な時間がかかることはなくなります。

注意しておくこと

Gemのコマンドを使う際にはbundle execを付け足すのを忘れないようにしてください。
こんな感じで↓

$ docker-compose run --rm app bundle exec rpsec

え?docker-compose run --rm appだけでも長いのにbundle execまで毎回付けるのは面倒くさいって?
alias設定するなりMakefile使うなりやりようはいくらでもあるじゃないですか。

おわりに

僕自身エンジニア歴1年ちょっと、Dockerを使い始めて2ヶ月程度なので知識不足が否めません。
見当違いの事を言っている可能性も十分にあります。
誤った表現や設定等ありましたらコメントにてご指摘をお願いします。

Docker便利ですね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル学習メモ3

renderの中身はルーティングパスじゃなくてcontrollerの階層を記述する

render("users/login_form")

viewで <%= @user.id %> などとやらずにechoする方法

      <% 
      if @user.id == @current_user.id
        concat link_to("編集", "/users/#{@user.id}/edit")
      end
      %>

concatを使う。なんでputsとかprintとかできないんだろ。

簡単にループ

      <%
        10.times do
          concat "cat"
        end
      %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでmodelに動的なカウンターをつける

要約

railsで一覧画面に何かしらの集計値とかフラグを表示したいときがあると思います。
いいね数とかそういうの。
それをcounter_cacheとかでデータとして持たずに、動的に集計してきれいに組み込む方法です。

やり方

class Post < ApplicationRecord
  has_many :likes, as: :likable
end

class Like < ApplicationRecord
  belongs_to :likable, polymorphic: true
end

@posts = Post.all.select("posts.*, (select count(*) from likes l where l.likable_type='Post' and l.likable_id=posts.id) as likes_count")

@posts.first.likes_count # as xxxがattributeになる
=> 3

サブクエリ(select count(*) from likes l where l.likable_type='Post' and l.likable_id=posts.id)の中は好きなように書けるので、かなり汎用性高くフラグなりなんなりつけられるので、これだけ覚えておけば集計付き一覧はだいたいできると思います。

個人的にrailsのjoin+preload/eager_load/includesなどを使ってrailsのオブジェクトとしてしっかりつくる、という方法にこだわってきたのですが、これだとassociationの書かれ方に影響されて毎回違う書き方になってしまいます。フラグがひとつふたつ付けばいい場合にはやりすぎになってしまいがちです。
一方で、この書き方は多少生のsql書いてしまいますが、使い回しが効くのではないかと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのGeneratorGeneratorが便利すぎたので早く帰れる

どうも、株式会社Fusicでプリンシパルエンジニアやってる南部です。この記事はFusic その2 Advent Calendar 2019の2日目の記事です。

経緯

エンジニアは怠惰であれ、という言葉は誰が言ったのか、知るよしもありませんが、私もその例にもれず、怠惰な人間です。
ルーチンで作るものは5秒でつくってしまって、本当に頭を使うべきものに時間を割きたいと思うのは私だけではないでしょう。

RailsにはGeneratorをGenerateするGeneratorGeneratorというものがあります。もう何を言ってるのかわかりませんが、とにかくそういうものがあるのです。

これを導入すると、ルーチンワークを5秒で終わらせる夢が叶います。
みなさんも是非お試しください。

GeneratorGeneratorでGeneratorのテンプレートを作ろう

$ bundle exec rails generate generator nantoka
      create  lib/generators/nantoka
      create  lib/generators/nantoka/nantoka_generator.rb
      create  lib/generators/nantoka/USAGE
      create  lib/generators/nantoka/templates

このようにlib/generators/nantokaの中にファイルやディレクトリが作られます。
本体になるのはnantoka_generatorです。

lib/generators/nantoka/nantoka_generator.rb
class NantokaGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)
end

まず、試しにこんなコードを書いてみます。

lib/generators/nantoka/nantoka_generator.rb
class NantokaGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  def step1
    puts "step1: #{self.name}"
  end

  def step2
    puts "step2: #{self.name}"
  end
end
$ bundle exec rails g nantoka hoge                                                                                                                                                                                                                            
step1: hoge
step2: hoge

おわかりの通りに、このGeneratorに実装されたメソッドが順番に実行されるようです。

ファイルを作る

ファイルを作るにはcreate_fileを使って実装します。

lib/generators/nantoka/nantoka_generator.rb
class NantokaGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  def create_generator_test_file
    create_file "generator_test.txt", "これはgeneratorのテストです: #{self.name}"
  end
end

create_fileは第一引数がRailsのrootパスからの相対パスを指定し、第二引数はそのファイルに書かれる内容です。

$ bundle exec rails g nantoka hoge
    create  generator_test.txt
$ cat generator_test.txt
これはgeneratorのテストです: hoge

ちゃんとファイルができてますね。

テンプレートからファイルを作る

create_fileはちょっとした内容をファイルに出力するには便利ですが、コードを出力するには結構難ありです。
やっぱりテンプレートエンジンほしくないですか?ほしいですよね?
ほら、我らがERBがあるじゃないですか。

lib/generators/nantoka/nantoka_generator.rb
class NantokaGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  def create_generator_test_file
    template "generator_test.txt.erb", "generator_test.txt"
  end
end
lib/generators/nantoka/templates/generator_test.txt.erb
this is test

<%= name %>

これを実行してみましょう。

$ bundle exec rails g nantoka hoge
    create  generator_test.txt
$ cat generator_test.txt
this is test

hoge

引数がほしい

引数がほしい?じゃあ、作ればいいじゃない。

lib/generators/nantoka/nantoka_generator.rb
class NantokaGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  # 引数を追加
  argument :words, type: :array, default: []

  def create_graphql_schema
    template "generator_test.txt.erb", "generator_test.txt"
  end
end

lib/generators/nantoka/templates/generator_test.txt.erb
this is test

<%= name %>

<% words.each do |word| -%>
<%= word %>
<% end -%>
$ bundle exec rails g nantoka hoge arg-foo arg-bar
    create  generator_test.txt
$ cat generator_test.txt
this is test

hoge

arg-foo
arg-bar

実際何に使うか?

例えば、graphql-rubyを使ってスキーマ定義したりするとき、似たようなRubyのコードがいっぱい出てくると思います。
そういうときに、一つGeneratorをつくっておけば、あら簡単。
どこかでバグが見つかっても、作り直すことも簡単です。

時間を無駄に浪費して残業してしまうなんてことがなくなりますように。

まとめ

エンジニアとは自らの幸せを自ら掴み取る人間である

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存のRailsアプリをDocker上で起動させる

はじめに

railsを勉強しているのですが、勉強のために、サンプルコードなどを自分の環境上で動かしたかったのですが、
githubなどに上がっているサンプルコードをdocker上で立ち上げるのに苦労したのでメモします。

新規のプロジェクトを立ち上げる際のDockerFileの書き方は情報がたくさんあったのですが、
既存のものを立ち上げる際の情報はほとんど無くて、あってもRuby2.4のものだったりしたので、
いろいろなサイトを参考にして、Ruby2.5環境で既存のプロジェクトを立ち上げるためのコンテナ構築をしました。

Ruby2.5でのDocker環境構築

ディレクトリ構造(完成形)
workディレクトリでrailsアプリ(DockerFileのないもの)をクローンします。

sampleapp/
 ├ work
 │ └ app/
 │ └ config/
 │ └ ...
 │  
 ├ Dockerfile
 └ docker-compose.yml

作業用ディレクトリを作成し、GithubからRailsプロジェクト(DockerFileのないもの)をクローンします。
クローンしたら、dockerコマンドを入力するためにsampleappディレクトリに戻ります。

$ cd
$ mkdir sampleapp
$ cd sampleapp
$ mkdir work
$ cd work
$ git clone ....(URLを入れる)
$ cd ..

Dockerfile

FROM ruby:2.5
#日本語対応
ENV LANG C.UTF-8
#作業用ディレクトリを作成
ENV ROOT_PATH /work
RUN mkdir -p $ROOT_PATH
WORKDIR $ROOT_PATH
#Railsアプリに必要なパッケージをインストールする
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
        && apt-get install -y nodejs build-essential libpq-dev\
     && rm -rf /var/lib/apt/lists/*
#Rspec用chromedriver
RUN apt-get update && apt-get install -y unzip && \
    CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` && \
    wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/ && \
    unzip ~/chromedriver_linux64.zip -d ~/ && \
    rm ~/chromedriver_linux64.zip && \
    chown root:root ~/chromedriver && \
    chmod 755 ~/chromedriver && \
    mv ~/chromedriver /usr/bin/chromedriver && \
    sh -c 'wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' && \
    sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' && \
    apt-get update && apt-get install -y google-chrome-stable

ADD ./work/Gemfile $ROOT_PATH/Gemfile
ADD ./work/Gemfile.lock $ROOT_PATH/Gemfile.lock

RUN gem install bundler
RUN bundle install


ADD ./work $ROOT_PATH

docker-compose.yml

version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_USER: root
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
    ports:
      - "3306:3306"

  web:
    build: .
    command: bundle exec rails s -b 0.0.0.0
    environment:
    volumes:
      - ./work:/work:cached
    ports:
      - "3000:3000"
    links:
      - db

railsアプリのconfig/database.ymlでデータベースとの接続情報を編集します。
以下、work/config/database.yml

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: 
  host: db

# development環境だけ書き換えてます。
development:
  <<: *default
  username: root
  password: 
  database: docker_development

test:
  <<: *default
  database: docker_test

production:
  <<: *default
  database: docker_production
  username: root
  password: <%= ENV['DATABASE_PASSWORD'] %>

設定が終わったらビルドします。

$ docker-compose build
$ docker-compose exec web rails db:create db:migrate

railsアプリに必要なパッケージやgemがインストールできるので、
終わったらdocker-compose upで起動させます。

localhost:3000でアプリのトップページにアクセスできます。

3306や、3000の部分はポートがかぶらないように、お好みの番号に設定できます。
※指定できないポートもあるので、エラーが出る際は下記のサイトなどを参考にするといいと思います。

開発中、ChromeでERR_UNSAFE_PORTエラーが出たときにチェックすべきこと(312エラー):http://nanoappli.com/blog/archives/7772

あとは新規からアプリを作成する時と同じように開発できます。

参考にしたサイト

既存railsプロジェクトのdocker運用開始時の作業録:https://www.dendoron.com/boards/50
Docker+既存Rails(+Puppeteer) やっぱりdockerで環境作るのを諦められなかった話:https://note.com/mick_sato/n/nfb521d6b2a4c
開発中、ChromeでERR_UNSAFE_PORTエラーが出たときにチェックすべきこと(312エラー):http://nanoappli.com/blog/archives/7772

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyのmap.flatten(1)よりもflat_mapの方が高速な理由をふわっと調べてみる

概要

タイトルの通りですが、具体的にソースを見るまで納得できなかったので調べてみました。

Rubyのソースコードを見てみる

本当はバージョンとかも考慮したほうがいいですが、下記リポジトリの最新版で見てみます。

https://github.com/ruby/ruby

まずはflat_mapを見てみる

どうやらEnumeratorのソースはenum.cにありそうでしたので、flat_mapに関連する箇所を確認してみました。
単純に配列を生成して、yieldした結果を格納しているというシンプルな内容でした。

enum.c
enum_flat_map(VALUE obj)
{
    VALUE ary;

    RETURN_SIZED_ENUMERATOR(obj, 0, 0, enum_size);

    ary = rb_ary_new();
    rb_block_call(obj, id_each, 0, 0, flat_map_i, ary);

    return ary;
}

flat_map_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, ary))
{
    VALUE tmp;

    i = rb_yield_values2(argc, argv);
    tmp = rb_check_array_type(i);

    if (NIL_P(tmp)) {
    rb_ary_push(ary, i);
    }
    else {
    rb_ary_concat(ary, tmp);
    }
    return Qnil;
}

map.flatten(1)を見てみる

こちらは挙動そのままなので言わずもがなでした。

enum.c
# map
static VALUE
enum_collect(VALUE obj)
{
    VALUE ary;
    int min_argc, max_argc;

    RETURN_SIZED_ENUMERATOR(obj, 0, 0, enum_size);

    ary = rb_ary_new();
    min_argc = rb_block_min_max_arity(&max_argc);
    rb_lambda_call(obj, id_each, 0, 0, collect_i, min_argc, max_argc, ary);

    return ary;
}
hash.c
# flatten

static VALUE
rb_hash_flatten(int argc, VALUE *argv, VALUE hash)
{
    VALUE ary;

    rb_check_arity(argc, 0, 1);

    if (argc) {
    int level = NUM2INT(argv[0]);

    if (level == 0) return rb_hash_to_a(hash);

    ary = rb_ary_new_capa(RHASH_SIZE(hash) * 2);
    # flatten(1)なのでここが1回だけ実行される
    rb_hash_foreach(hash, flatten_i, ary);
    level--;

    if (level > 0) {
        VALUE ary_flatten_level = INT2FIX(level);
        rb_funcallv(ary, id_flatten_bang, 1, &ary_flatten_level);
    }
    else if (level < 0) {
        /* flatten recursively */
        rb_funcallv(ary, id_flatten_bang, 0, 0);
    }
    }
    else {
    ary = rb_ary_new_capa(RHASH_SIZE(hash) * 2);
    rb_hash_foreach(hash, flatten_i, ary);
    }

    return ary;
}

static int
flatten_i(VALUE key, VALUE val, VALUE ary)
{
    VALUE pair[2];

    pair[0] = key;
    pair[1] = val;
    rb_ary_cat(ary, pair, 2);

    return ST_CONTINUE;
}

結論

確かにmap.flatten(1)の方が配列の生成、ループ処理が1回づつ多かったので、早くなるのは納得でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JScript 覚書(実践編その壱)

この記事は、富士通ソーシアルサイエンスラボラトリ Advent Calendar 2019の 3 日目の記事です。

はじめに

@GORO_Nekoです。ご存知の方ご無沙汰してます。初めての方お初にお目にかかります。

えーっと、先にお断りをば一言。

以下は、自分が所属する会社の意向を反映したものでもスタンスを示すものでもなく、単なる一個人の趣味の活動から産まれた記述です。

JScriptって知ってます?

知らない方、まずこちらの記事をご覧ください。

てぇわけでこの記事、じつは単品記事じゃなくて上で紹介した記事「JScript 覚書」の続編デス。

前の記事で「JScript とは何か」および「JScript で Windows の標準入力・標準出力・標準エラー出力を利用する方法」を紹介してみたつもりでいたのですが、読み返してみるとなんか座りが悪い。

使い方の説明として、若干コードも載っけてみたけど、どうも中途半端に感じちゃったんですよね。

てなわけで、もう少し意味のあるコードを記載して、「JScript で Windows の標準入力・標準出力・標準エラー出力を利用する方法」をコードを通して再解説しようと思います。

数あてゲームを作ってみる

正式名称かどうか実はよくわかっていませんが「数あてゲーム」ってありますよね?

コンピュータが考えた数値がいくつか、言い当てるゲーム。

人間が「コンピュータが考えた数は、xxx だろ?」とキーボードを通して通知すると、コンピュータが「もっと大きな数」「もっと小さな数」「あたり!」を答えるアレです。

以下、JScript で実装した数あてゲームのソースコードを掲載します(注意: 入力内容のチェック等エラーチェック処理ちゃんとやってません(:p )。

コード中のコメント等を読んで「JScript で Windows の標準入力・標準出力・標準エラー出力を利用する方法」を体感してみてください(体感してもらえるといいなぁ)。

// 0 ~ 999 の数字を一つ生成
var random_no = Math.floor(Math.random() * 1000);

// 人間が撃ち込んだ数値を読み取る変数を用意する
var input_number = 0;

// 人間が行った発言回数を記録するカウンタを用意する
var cnt = 0;

// 発言数の上限を定義する
var MAX_CNT = 20;

// あたり判定フラグを用意する
var hit_flg = false;

// コンピュータが返すメッセージ文字列を定義する
var OOKII  = "大きいです。";
var TIISAI = "小さいです。";
var ATARI  = "あたりです。";

// コンピュータが返すメッセージの格納変数を用意する
var pcAns = "";

// 開始メッセージを出力する
WScript.StdOut.WriteLine("0~999の数字を入力してください。");
WScript.StdOut.WriteLine("私の考えた数より大きい数だった場合「大きいです。」");
WScript.StdOut.WriteLine("私の考えた数より小さい数だった場合「小さいです。」");
WScript.StdOut.WriteLine("私の考えた通りの数だった場合「あたりです。」とお答えします。");

while(cnt < MAX_CNT){

    // ユーザに入力を促す
    WScript.StdOut.WriteLine("数を入力して[Enter]キーを押してください。");

    // 標準入力に入力された情報を読み取る
    input_number = WScript.StdIn.ReadLine();

    //当たりはずれを判定する
    if(random_no < input_number){
        pcAns = OOKII;
    }else if(random_no > input_number){
        pcAns = TIISAI;
    }else{
        pcAns = ATARI;
        hit_flg = true;
    }

    // 結果表示
    WScript.StdOut.WriteLine(pcAns);

    // 状態確認処理
    if(true == hit_flg){
        // ループを脱出する
        break;
    }else{
        // 入力数カウンタをカウントアップしてループ処理を続ける
        cnt++;
    }

}

// 終了処理
if(true == hit_flg){
    WScript.StdOut.WriteLine("おめでとうございます。" + (cnt + 1) + "回で正解です。");
}else{
    WScript.StdOut.WriteLine("残念。" + MAX_CNT + "回以内で正解できませんでした。");
}

先の記事で解説していますが、一応上記のコードの実行方法を書きます。

上記コードを "kaduate.js" ファイルに書き込んだとします。

その場合、実行方法は以下のようになります。

x:\> cscript kaduate.js

cscript にマイクロソフトのロゴを出させたくない場合は以下の通り。

x:\> cscript /nologo kaduate.js

うまく動きましたでしょうか?

なお、いつものごとく Ruby で書くとこんな感じ…かな?

require 'readline'

#
# 注意:
# windows OS 上で実行する場合は、管理者モードで起動し、以下のコマンドを実行したCMD上で実行すること
# > chcp 650001
# このコード自体はエンコード UTF-8 でファイル化して実行のこと
#

# 0 ~ 999 の数字を一つ生成
random_no = Random.new.rand(0..999)

# 人間が撃ち込んだ数値を読み取る変数を用意する
input_number = 0;

# 人間が行った発言回数を記録するカウンタを用意する
cnt = 0;

# 発言数の上限を定義する
MAX_CNT = 20;

# あたり判定フラグを用意する
hit_flg = false;

# コンピュータが返すメッセージ文字列を定義する
OOKII  = '大きいです。'
TIISAI = '小さいです。'
ATARI  = 'あたりです。'

# コンピュータが返すメッセージの格納変数を用意する
pcAns = "";

# 開始メッセージを出力する
p '0~999の数字を入力してください。'
p '私の考えた数より大きい数だった場合「大きいです。」'
p '私の考えた数より小さい数だった場合「小さいです。」'
p '私の考えた通りの数だった場合「あたりです。」とお答えします。'

while cnt < MAX_CNT do

    # ユーザに入力を促す
    p '数を入力して[Enter]キーを押してください。'

    # 標準入力に入力された情報を読み取る
    input_number = Readline.readline.to_i
    # 当たりはずれを判定する
    if random_no < input_number then
        pcAns = OOKII
    elsif random_no > input_number then
        pcAns = TIISAI
    else
        pcAns = ATARI
        hit_flg = true
    end

    # 結果表示
    p pcAns

    # 状態確認処理
    if true == hit_flg then
        # ループを脱出する
        break
    else
        # 入力数カウンタをカウントアップしてループ処理を続ける
        cnt = cnt + 1
    end

end

# 終了処理
if true == hit_flg then
    p 'おめでとうございます。' + (cnt + 1).to_s + '回で正解です。'
else
    p '残念。' + MAX_CNT.to_s + '回以内で正解できませんでした。'
end

では、また。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Stripe #API でカスタマー = 顧客を作成・取得する ( 公式ドキュメントのまま ) ( #Ruby )

Command

Ruby

require 'stripe'
Stripe.api_key = 'sk_test_xxxxx'

Stripe::Customer.create({
  description: 'Customer for jenny.rosen@example.com',
})

curl

curl https://api.stripe.com/v1/customers -u sk_test_xxxxx: -d description="Customer for jenny.rosen@example.com"

API Doc

https://stripe.com/docs/api/customers/create?lang=curl

example

$ curl -s curl https://api.stripe.com/v1/customers -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: -d description="Customer for jenny.rosen@example.com"
{
  "id": "cus_GHaC8VxZWYuwcl",
  "object": "customer",
  "account_balance": 0,
  "address": null,
  "balance": 0,
  "created": 1575239418,
  "currency": null,
  "default_source": null,
  "delinquent": false,
  "description": "Customer for jenny.rosen@example.com",
  "discount": null,
  "email": null,
  "invoice_prefix": "B92698A0",
  "invoice_settings": {
    "custom_fields": null,
    "default_payment_method": null,
    "footer": null
  },
  "livemode": false,
  "metadata": {
  },
  "name": null,
  "phone": null,
  "preferred_locales": [

  ],
  "shipping": null,
  "sources": {
    "object": "list",
    "data": [

    ],
    "has_more": false,
    "total_count": 0,
    "url": "/v1/customers/cus_GHa5lNIgPs4wF3/sources"
  },
  "subscriptions": {
    "object": "list",
    "data": [

    ],
    "has_more": false,
    "total_count": 0,
    "url": "/v1/customers/cus_GHa5lNIgPs4wF3/subscriptions"
  },
  "tax_exempt": "none",
  "tax_ids": {
    "object": "list",
    "data": [

    ],
    "has_more": false,
    "total_count": 0,
    "url": "/v1/customers/cus_GHa5lNIgPs4wF3/tax_ids"
  },
  "tax_info": null,
  "tax_info_verification": null
}

ダッシュボードで確認

https://dashboard.stripe.com/test/customers

image

発行された customer_id がURLにも反映されるみたいだ

https://dashboard.stripe.com/test/customers/cus_GHaC8VxZWYuwcl

image

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2796

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails+DeviseへのOmniauthの導入(ざっくり仕組み、CSRF対策、単体テスト含む)

内容

 RailsアプリにOmniauth認証(google_oauth2, facebook)を導入する方法とその過程で学んだことを紹介する記事です。全体の流れやコードの意図を説明した記事はあまり見つからなかったと思ったので、その辺りを中心に解説したいと思います。

対象

 rails初心者でOmniauth認証の導入に挑む人。

 もし某スクールの後輩で見てくれた人がいた場合、
 非常に面白い機能なので、まずは自力で挑戦することをお勧めします。
 (そもそも違う環境でちゃんと動くとも、正解とも限りません。。。)
 少しでもご参考になれば、と思って書きます。

前提条件

-ruby 2.5.1p57
-Rails 5.2.3
-gem 'devise' 4.7.1
(ローカル環境のみの対応です)
 

1.そもそも

 omniauthでは、あるアプリにおけるログイン認証の代わりや、外部機能の使用許可をすることができます。
本実装においては、前者の機能を使用しています。
流れとしては、本アプリのパスワード入力を、SNSへのログイン(≒Cookieによりほぼ自動ログイン)で代用するイメージです。以下に全体図のイメージを示します。
 Qiita Oauth用資料.001.jpeg
青矢印のフローを実装していきます!

2.実装

2-1.gemの導入

まず、gemfileに以下を追記後、bundle installを実施します。

Gemfile
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'

2-2.設定

各SNSサイト(Google developers console, facebook for developers)にて、
URLの登録および、ID, SECRET KEYを取得します。
(ご参考サイト様:https://qiita.com/hidepino/items/a1eb9d2f32ce33389f20)

環境変数を設定します。

config/initializers/devise.rb
config.omniauth :facebook, ENV['FACEBOOK_ID'], ENV['FACEBOOK_KEY']
config.omniauth :google_oauth2, ENV['GOOGLE_ID'], ENV['GOOGLE_KEY']

ターミナルにて、"vim ~/.bash_profile"を実行し、取得したIDとキーを記入します。

bash_profile
export FACEBOOK_ID="取得したID"
export FACEBOOK_KEY="取得したキー"
export GOOGLE_ID="取得したID"
export GOOGLE_KEY="取得したキー"

"source ~/.bash_profile"を実行し、環境変数を有効化しましょう。

2-3.routingの設定

 SNS側からcallbackが来た際に使用するコントローラーを定義してあげます。

routes.rb
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

2-4.callbackコントローラーの作成、記述 (イメージ図の手順④⑤に当たります)

 "rails g devise:controllers users"を実行し、users/omniauth_callbacks_controllerを作成し、以下を記述します。ここでは、callbackが来た際に行うアクションを設定しています。SNS側から来た情報であるauth_hashは、request.env["omniauth.auth"]として使用していきます。
クラスメソッド"from_omniauth"は次のステップでUser.rbに定義します。

controllers/users/omniauth_callbacks_controller.rb
def facebook
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?  #もし@userがDBに既にいたら、ログイン状態にします  
      sign_in_and_redirect @user, event: :authentication 
      set_flash_message(:notice, :success, kind: 'Facebook') if is_navigational_format?
    else #もし@userがDBにいない場合、新規登録ページにリダイレクトします
      session["devise.facebook_data"] = request.env["omniauth.auth"]
    #データをsessionに入れることによって、新規登録ページの入力欄に、予め情報を入れておくなどが可能になります。
      redirect_to 新規登録ページ
    end
  end

  def google_oauth2
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication 
      set_flash_message(:notice, :success, kind: 'google') if is_navigational_format?
    else
      session["devise.google_data"] = request.env["omniauth.auth"][:info]
      #google認証の場合は、なぜかauth_hashの容量が大きく、一瞬で容量オーバーとなるため、新規登録時に必要な情報のみをsessionに渡すこととしました。(おそらく画像データのせい?)
      redirect_to 新規登録ページ
    end
  end

2-5.メソッドの定義 (イメージ図の手順④⑤に当たります)

ユーザー登録の流れを設定します。 ここは設計により異なります!

既存ユーザーであるかの識別は,uidやemailアドレスにて、実施されている記事を多く見受けましたが、
本実装では、usersテーブルとsns_credentialsテーブルを別で用意したため、
(1人のuserが複数のsns_credentialsを持つことを想定しています。)
ユーザーの識別はemailで行うことにしました。

さらに、既存ユーザーがいなかった場合に関して、
ここでuser, sns_credentialをDBへ登録することもできますが、
本アプリにて必要な情報が、auth_hash上で欠けている場合を想定し、
インスタンスの作成に留めました。

models/user.rb
def self.from_omniauth(auth)
    user = User.where(email: auth.info.email).first
    sns_credential_record = SnsCredential.where(provider: auth.provider, uid: auth.uid)
    if user.present?
      unless sns_credential_record.present?
        SnsCredential.create(
          user_id: user.id,
          provider: auth.provider,
          uid: auth.uid
        )
      end
    elsif
      user = User.new(
        id: User.all.last.id + 1,
        email: auth.info.email,
        password: Devise.friendly_token[0, 20],
        nickname: auth.info.name,
        last_name: auth.info.last_name,
        first_name: auth.info.first_name,
      )
      SnsCredential.new(
        provider: auth.provider,
        uid: auth.uid,
        user_id: user.id
      )
    end 
  user
  end

2-6.リンクの導入

最後にViewにリンク先を記入して終了です!!

view.html.erb
<%= link_to "Sign in with Facebook", user_facebook_omniauth_authorize_path %>
<%= link_to "Sign in with Google", user_google_oauth2_omniauth_authorize_path %>

2-7.CSRF対策

と言いたいところですが、Omniauth認証はCSRF脆弱性が指摘されているので、
以下の対策用のgemを導入し、リンクの書き方を変更して、本当の終了です。

Gemfile
gem "omniauth-rails_csrf_protection"
rspec/view.html.erb
<%= link_to "Sign in with Facebook", user_facebook_omniauth_authorize_path, method: :post %>
<%= link_to "Sign in with Google", user_google_oauth2_omniauth_authorize_path, method: :post %>

3.テストコード(一例)

 対象:sns_credential.rbのuidのunique制約が作動するか
ダミーのauth_hashを作成したり、omniauthをtestモードにするなど、少し設定が必要です。
 
↓設定

rails_helper.rb
module OmniauthMocks
  def facebook_mock
    OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new(
      {
        provider: 'facebook',
        uid: '12345',
        info: {
          name: 'mockuser',
          email: 'sample@test.com'
        },
        credentials: {
          token: 'hogefuga'
        }
      }
    )
  end
end


RSpec.configure do |config|
  OmniAuth.config.test_mode = true
  config.include OmniauthMocks
end

↓テストコード

spec/models/sns_credentials_spec.rb
RSpec.describe SnsCredential, type: :model do
  describe  '#facebook validation' do
    before do
      Rails.application.env_config['omniauth.auth'] = facebook_mock
    end
    context '認可サーバーから返ってきたメールアドレスを、すでに登録済みのuserが持っていた場合' do
      before do
        user = create(:user, email: 'sample@test.com')
      end
      context '認可サーバーから帰ってきた情報とprovider名が異なるが、同じuidを持つSnsCredentialレコードがあった場合' do
        before do
          SnsCredential.create(provider: 'google_oauth2', uid: '12345', user_id: '1')
        end
          example 'uidのvalidation(unique制約)が機能するか' do
            expect(SnsCredential.create(provider: 'facebook', uid: '12345', user_id: '1').errors[:uid]).to include('はすでに存在します')
          end         
      end
    end
  end
end

4.考察

・設計が良くなかったと思いますが、結局、"本アプリに登録したemailアドレス"と"SNS側からトークンで帰って来るemailアドレス"の照合をしているだけと言えます。結果、SNSに登録したemailとパスワードがあれば、本アプリの認証をパスされてしまう事になるので、セキュリティ的な甘さを感じました。。。
(SNS側は別デバイスでのログインを見張る、SMS認証等、強固なようなので、そこは安心と思います)
対策としては、認証時にもう1ハードルが必要かもしれません。

・また、アドレスや住所等の個人情報がサーバーサイド側に飛ぶので、ユーザー目線としては、信頼できないサイトでは使うべきではない、と思いました。。。

5.参考にさせて頂いた記事様

https://github.com/plataformatec/devise/wiki/OmniAuth%3A-Overview
https://github.com/mkdynamic/omniauth-facebook/blob/master/README.md
https://github.com/zquestz/omniauth-google-oauth2/blob/master/README.md
https://github.com/cookpad/omniauth-rails_csrf_protection
https://qiita.com/hidepino/items/a1eb9d2f32ce33389f20

長文にも関わらず、最後までお読みいただきありがとうございました。:bow_tone3:

初投稿記事なので、ご意見、修正点などいただけましたら、幸いです!:blush:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 のちょい足しな新機能を試す109(while_preventing_writes 編)

はじめに

Rails 6 に追加された新機能を試す第109段。 今回は、while_preventing_writes 編です。
Rails 6 では、 while_preventing_writes が追加されました。

multi-db 関連のメソッドで、 while_preventing_writes のブロック内では、 DBへの書き込みができません。
同じDBに対してDBのConnection を別に作成しても書き込みはできないようになっています。

Ruby 2.6.5, Rails 6.0.0 で確認しました。

$ rails --version
Rails 6.0.0

今回は、簡単なスクリプトを作って確認します。

Rails プロジェクトを作成する

$ rails new rails_sandbox
$ cd rails_sandbox

User モデルを作成する

User モデルを作成します。

$ bin/rails g model User name

User モデルを編集する

User モデルのDBの Connection を ActiveRecord::Base.connection とは別になるように変更します。

app/models/user.rb
class User < ApplicationRecord
  connects_to database: { writing: :primary, reading: :primary }
end

動作確認のスクリプトを作成する

ActiveRecord::Base.connectionUser.connection が違うことを確認し、 while_preventing_writes のブロック内では、DBへの書き込みができないことを確認します。

scripts/while_preventing_writes.rb
puts User.count

if ActiveRecord::Base.connection.object_id != User.connection.object_id
  puts 'ActiveRecord::Base.connection != User.connection'
end

# ActiveRecord::Base.connection.while_preventing_writes do # Rails 6.0.0.rc1
ActiveRecord::Base.connection_handler.while_preventing_writes do # Rails 6.0.0
  User.create!(name: 'Taro')
end

puts User.count

マイグレーションを実行する

$ bin/rails db:create db:migrate

スクリプトを実行する

スクリプトを実行します。 Write query attempted while in readonly mode: のメッセージが出力され、書き込みが失敗することがわかります。

$ bin/rails runner scripts/while_preventing_writes.rb
Running via Spring preloader in process 81
0
ActiveRecord::Base.connection != User.connection
Traceback (most recent call last):
...
/usr/local/bundle/gems/activerecord-6.0.0/lib/active_record/connection_adapters/postgresql_adapter.rb:643:in `execute_and_clear': Write query attempted while in readonly mode: INSERT INTO "users" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" (ActiveRecord::ReadOnlyError)

ちなみに

Rails 6.0.0rc1 では、エラーにならず、保存できてしまいます。(6.0.0rc1 では、メソッドの定義場所が異なるため、 ActiveRecord::Base.connection.while_preventing_writes とする必要があります。)

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try109_while_preventing_writes

参考情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reformで親子関係のあるフォームオブジェクトを作ってみる

Ateam cyma Adevent Calendar 2019、4日目です!
本日は株式会社エイチームでcymaのエンジニアの @bayasist が務めさせていただきます。

Ruby on Railsで書かれたプログラムでは、フォームオブジェクトを利用することで、分かりやすく書くことができる場合があります。RailsでFormObjectを簡単に作れるtrailblazerというgemのReformというものがあり、使いこなせばなかなか便利です。
フォームオブジェクトが楽に作れるようにいくつか機能はありますが、ドキュメント(特に日本語)が少ないので、今回は特に親子関係をもつデータのReformを用いたフォームオブジェクトの作り方を解説できたらと思います。

フォームオブジェクトを利用する利点

フォームオブジェクトはmodelをそのままformにするのではなく、バリデートなどformに関係する処理を行うオブジェクトです。これらのオブジェクトを作成することで、以下のようなメリットがあります。

  • 複数モデルをまたがったFormの処理をコントローラに書かなくても済む
  • ActiveRecordのモデル以外のデータの更新などでも同じようなお作法でView,Controllerが書ける

Reformのメリット

Reformは下記のような特徴があります

フォームオブジェクトを簡易的に作成できるtrailblazerというgemのReformというものがあります。それを用いると下記のようなメリットを受けることができます。

  • ActiveModelと同じような記法でValidateや要素などが記載できる
  • ActiveModel以外のデータの読み書きでも同様の記法で扱える
  • 親子関係など多少データ構造が多少複雑になってもプログラムが複雑になることはない

まずはReformでフォームオブジェクトの基本形を作る

まずは、一つのmodelのみでReformを使ったフォームオブジェクトを作っていきます。nameカラムを持ったparentというモデルを更新するだけのものを作ります。(あとでchildというモデルをparentの子供にします)

app/models/parent.rb
class Parent < ApplicationRecord
end
app/forms/parent_form.rb
class ParentForm < Reform::Form
  property :name
  validates :name, length: { maximum: 5}
end
app/controllers/parent_controller.rb
class ParentController < ActionController::Base
  def edit
    @form = form
  end

  def update
    @form = form
    if @form.validate(update_param)
      @form.save
    end
  end

  private 

  def form
    ParentForm.new(Parent.find(params[:id]))
  end

  def update_param
    params.require(:parent).permit(:name)
  end
end
app/views/parent/edit.html.rb
<%= form_with model: @form do |form| %>
  <%= form.text_field :name %>
  <%= form.submit %>
<% end %>

formオブジェクトができました。
Controllerを見ていただきたいのですが、ほとんどActiveRecordのお作法で書くことができます。
一点大きく違うところはvalidateの部分。Reformでは、validateメソッドでパラメータのバリデートをし、フォームオブジェクトの各要素ににパラメータを渡していきます。
またsaveメソッドでは、フォームオブジェクトの値をもとのモデルに渡してから、モデルのsaveメソッドが呼び出されています。モデルのsaveメソッドを呼び出さず、元のモデルへのデータの移動のみをやりたい場合はsyncメソッドを用います。

parentモデルにchildという子要素を作る

parentモデルの子要素としてchildモデルを作ります。

app/models/parent.rb
class Parent < ApplicationRecord
  has_many :children
end
app/models/child.rb
class Child < ApplicationRecord
  belongs_to :parent
end
app/forms/parent_form.rb
class ParentForm < Reform::Form
  property :name
  validates :name, length: { maximum: 5}
  collection :children, populate_if_empty: Child do
    property :name
    validates :name, length: { maximum: 5}
  end
end
app/controllers/parent_controller.rb
class ParentController < ActionController::Base
  def edit
    @form = form
  end

  def update
    @form = form
    if @form.validate(update_param)
      @form.save
    end
  end

  private 

  def form
    ParentForm.new(Parent.includes(:children).find(params[:id]))
  end

  def update_param
    params.require(:parent).permit(:name, children_attributes: [:name])
  end
end
app/views/parent/edit.html.rb
<%= form_with model: @form do |form| %>
  <%= form.text_field :name %><br />
  <%= form.fields_for :children do |child_form| %>
    <%= child_form.text_field :name %><br />
  <% end %>
  <%= form.submit %>
<% end %>

Formオブジェクトでcollectionを使うこと以外はActiveRecordを使用したときとほとんど変わらずに実装できます。
複数モデルのバリデーションや保存処理はフォームオブジェクトが引き受けるため、モデルが複数になってもControllerが散らかったりすることなく記述できます。また、Reformではそれらの処理をほとんど書くことなく行えます。

上記プログラムの重要な問題点

上記プログラムではChildのアップデートの際に、Textboxの値を上からDBで検索された順にあてはめていきます。/editの表示からアップデートまでにほかのブラウザなどでChildの一部要素がdeleteやinsert等されると予期せぬ動作につながります。
そこで、/editの表示の際にHiddenFieldにchildのidを入れておき、保存の際にChildのidとPOSTで送られてきたidを突合しながら保存していく必要があります。
そのために下記のプログラムを変更する必要があります。

app/controllers/parent_controller.rb
class ParentController < ActionController::Base
  # (中略)
  def update_param
    params.require(:parent).permit(:name, children_attributes: [:id, :name])
  end
end
app/views/parent/edit.html.rb
<%= form_with model: @form do |form| %>
  <%= form.text_field :name %><br />
  <%= form.fields_for :children do |child_form| %>
    <%= child_form.text_field :name %><br />
    <%= child_form.hidden_field :id %>
  <% end %>
  <%= form.submit %>
<% end %>
app/forms/parent_form.rb
class ParentForm < Reform::Form
  property :name
  validates :name, length: { maximum: 5}
  collection :children, populate_if_empty: Child,
    populator: ->(fragment:, **) {
      children.find_by(id: fragment["id"].to_i)
    } do
    property :name
    validates :name, length: { maximum: 5}
  end
end

これを行うことでHiddenFieldのidとDBのIDが同一のものを更新するようになります。ほかのブラウザで該当レコードが削除されていた際はvalidateを行う際にエラーとなり、ほかの関係ないデータを更新しに行くということはありません。
ここではfragmentはvalidateの際に送られてきたデータ(今回でいうとformで入力したデータ)、childrenはDBから持ってきたデータとなります。
この機能を用いれば、複合キーなどID以外で突合することもできます。

例)

app/forms/parent_form.rb
class ParentForm < Reform::Form
  property :name
  validates :name, length: { maximum: 5}
  collection :children, populate_if_empty: Child,
    populator: ->(fragment:, **) {
      children.find(***_id: fragment["***_id"].to_i, ~~~_code: fragment["~~~_code"].to_i)
    } do
    property :name
    validates :name, length: { maximum: 5}
  end
end

ChildのFormを外だしする

Childが複雑になってきたら、Childのフォームを別のファイルに移したくなるかもしれません。そのようにFormObjectの子要素を外に出すことも可能です。

app/forms/parent_form.rb
class ParentForm < Reform::Form
  property :name
  validates :name, length: { maximum: 5}
  collection :children, populate_if_empty: Child,
    populator: ->(fragment:, **) {
      children.find_by(id: fragment["id"].to_i)
    }, form: ChildForm
end
app/forms/child_form.rb
class ChildForm < Reform::Form
  property :name
  validates :name, length: { maximum: 5}
end

他にもいろいろなことができます

Reformを使うことで、子要素の追加や削除、デフォルト値の設定等がModel,Controllerを大きく汚すことなく比較的簡単に行えます。また、ActiveRecord以外のインスタンスにも利用できるため、FormからDBと関係ないインスタンスにデータを移すときに重宝します。
もっと調べてみたい人は公式ドキュメントを見てみてくださいね。

最後に

Ateam cyma Adevent Calendar 2019 の 4日目、いかがでしたか。
5日目は cymaのインフラつよつよエンジニアの @ihsiek がSQLのチューニング入門の記事を書くそうですよ!SQL苦手な人、早く動くSQLを書きたい人は必見ですよ!

株式会社エイチームでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。

エンジニアとしての働き方に興味を持たれた方はcymaの Qiita Jobs をご覧ください。

そのほかの職種は、エイチームグループ採用サイトをご覧ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Classiの新卒エンジニア向け研修、「万葉研修」について

皆様こんにちは!この記事はClassi Advent Calendar 4日目の記事です。

新卒の小野優子(@yukoono)と申します。ポートフォリオチームで、高校生がやったことを記録し、振り返るための「ポートフォリオ」の開発を行っております。

5月にClassiにjoinして以来、合同会社Fjordさんの「Fjord boot camp」で2ヶ月の研修→社内で2ヶ月半の研修→チームに配属され業務へ、というフローで動いておりました。
今回は社内で受けた新卒研修、通称「万葉研修」について書いていきます。

万葉研修とは

株式会社万葉さんがgithub上に公開している、Ruby on Railsのプログラマーになるための教育プログラムです。リンクはこちら。25ステップ+αあり、エンジニアとしてClassiにjoinした新卒メンバーは、この研修プログラムに2ヶ月程かけて取り組みます。メンターとして、「ゼロからわかるRuby超入門」の著者であるigaigaさん、12/21のTokyoGirls.rbで登壇されるただあきさんをはじめとした社内のエンジニアの皆様にお世話になりました。

大まかな流れ

プログラムのステップ3を終えた後、ステップ4で自分が作りたいアプリの仕組みやDB構造を考えます。私の場合は、「暇な時間を使って、普段先延ばしにしていることをやるアプリ」をコンセプトに置きました。利用の流れとしては、
1. アプリに筋トレや英語の勉強など、「やりたいけれど緊急ではない」ことを登録する
2. 暇な時間ができたら、何分程度暇なのかを登録する
3. 所要時間に合わせて、やることをアプリが提案する
4. 終わった後、記録をアプリに入力し、保存する

という感じです。このアプリのペーパープロトタイピングやDB構造案を見せながら、メンターとどう実装するかを相談します。私の場合は最初にやりたいことをかなり盛りだくさんで考えていたため、「まずはやりたいことを登録するだけの、最小の機能で実装してみよう。研修が進むのに合わせて、コンセプトの要素を取り入れていこう」という話になりました。
このようにしてアプリの方向性を決定した後は、研修のステップに沿って実装していきます。方向性がそれぞれ違うため、同じ研修を受けていても出来上がるアプリは人によってかなり違ったものになります。例として、同期の@ruru8は社内で利用できる書籍サービスを作っていました。詳細は22日の投稿をお楽しみに。

研修期間中の過ごし方

  1. プログラムに沿って、アプリに機能を実装する
  2. 取り組んでいたステップの実装が終わる、または途中でもキリのいい単位で実装が行えたら、Githubの自分のリポジトリにローカルの内容をpushする
  3. 見て欲しいところにコメントを書いてプルリクエストを出し、メンターにレビューをお願いする
  4. LGTM(Looks Good To Me…レビューがOKであるという意味)がもらえれば次の実装に取り掛かる。修正が必要な場合は、ローカルで修正を行い、pushして再レビューをお願いする

という感じです。
わからないことがあったときは、会社で買っていただいている参考書籍を読むか、メンターや社内の先輩に質問します。参考書籍についてはこちらにまとめられています。
また、Classiはコミュニケーションツールとしてslackを使っておりまして、個人が分報チャンネルを持ち、困りごとや思ったことなどをつぶやく文化が活発です。分報についてはこちら。私も自分の分報である「times_ono」というチャンネルに研修の進捗を書くようにしておりまして、ここで質問するとチャンネルを見ていただいている先輩からよくアドバイスを頂けます。

image.png

これはtimes_onoの一幕でして、私がわからない!と言ったことに対して、同期の@hxrxchang君が席に助けに来てくれて、参考になるリンクも貼ってくれたところです。こんな感じで、気軽にSOSが出せて、助けてくれる人が社内にたくさんいる、すごく温かい環境です。

成果物

出来上がったアプリがこんな感じです。

image.png

私はレビューで時間をかけたため、ステップ4で考えていたコンセプトの実現は十分には行えず、シンプルに万葉研修のステップを進めたアプリ、という感じの成果物になりました。ただ、私の場合はレビューで得た知識が評価され、チームで研修内容の共有会を開かせていただきました。

研修のゴールは人それぞれです。私はステップ23まで+研修内容の共有会の開催がゴールになりましたが、今研修中のベトナム人の新卒二人は1ヶ月で早くもステップ25を完了し、オプション要件に取り組んでいます。また、フロントエンドのスキルがあるメンバーはRailsのプログラミングと並行してAngular jsで画面の作成を行い、操作性に優れた格好いいアプリを作っています。Classiの先輩方に協力してもらい、ユーザビリティテストを行なったメンバーもいます。

研修を終えて

研修を通して、「プログラマーの仕事の大部分は、人への気遣いである」という感想を持ちました。なぜなら、メンターからの指摘事項のほとんどは、「他のエンジニアがアプリをメンテナンスする時に分かりやすい/後で困らないコーディング」についてのアドバイスだったためです。

  • こうした方が、他のプログラマーが分かりやすい!
  • この方がRailsや他のgemのアップデートに関わらず使える!
  • このコメントをつけた方が、レビュアーや他のエンジニアに親切!

などのコメントをたくさん頂くうちに、プログラマーの仕事像が「黙々と実装だけ行えれば良い、他の人とは関わらない」から、「他の人へ気遣い、情報を伝達し、理解して貰えるコードを書く」に変わっていきました。この意識は、現在チームで仕事をする上でも、チームメンバーへの気遣いや、伝わりやすいプルリクエストという形で活きています。

終わりに

この研修について社内の先輩エンジニアに話したところ、「羨ましい」という声を多く頂きました。2ヶ月以上かけてコードの書き方を教えてもらえる機会はとても貴重で、多くのプログラマーは仕事の中でコードの書き方やプルリクエストの仕方を覚えていくそうです。手厚い研修プログラムを組んでいただいたigaigaさん、ただあきさん、わからないところを教えて頂いた先輩エンジニアの皆さん、また充実した研修を受けさせて貰える会社の懐の広さに感謝しつつ、この投稿を締めます。

明日の投稿は@onigraさんです。お楽しみに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Classiの新卒エンジニア向け研修について

皆様こんにちは!この記事はClassi Advent Calendar 4日目の記事です。

新卒の小野優子(@yukoono)と申します。ポートフォリオチームで、高校生がやったことを記録し、振り返るための「ポートフォリオ」の開発を行っております。

5月にClassiにjoinして以来、合同会社Fjordさんの「Fjord boot camp」で2ヶ月の研修→社内で2ヶ月半の研修→チームに配属され業務へ、というフローで動いておりました。
今回は社内で受けた新卒研修について書いていきます。

研修内容

株式会社万葉さんがgithub上に公開している、Ruby on Railsのプログラマーになるための教育プログラムを使っています。リンクはこちら
25ステップ+αあり、エンジニアとしてClassiにjoinした新卒メンバーは、この研修プログラムに2ヶ月程かけて取り組みます。メンターとして、「ゼロからわかるRuby超入門」の著者であるigaigaさん、12/21のTokyoGirls.rbで登壇されるただあきさんをはじめとした社内のエンジニアの皆様にお世話になりました。

大まかな流れ

プログラムのステップ3を終えた後、ステップ4で自分が作りたいアプリの仕組みやDB構造を考えます。私の場合は、「暇な時間を使って、普段先延ばしにしていることをやるアプリ」をコンセプトに置きました。利用の流れとしては、
1. アプリに筋トレや英語の勉強など、「やりたいけれど緊急ではない」ことを登録する
2. 暇な時間ができたら、何分程度暇なのかを登録する
3. 所要時間に合わせて、やることをアプリが提案する
4. 終わった後、記録をアプリに入力し、保存する

という感じです。このアプリのペーパープロトタイピングやDB構造案を見せながら、メンターとどう実装するかを相談します。私の場合は最初にやりたいことをかなり盛りだくさんで考えていたため、「まずはやりたいことを登録するだけの、最小の機能で実装してみよう。研修が進むのに合わせて、コンセプトの要素を取り入れていこう」という話になりました。
このようにしてアプリの方向性を決定した後は、研修のステップに沿って実装していきます。方向性がそれぞれ違うため、同じ研修を受けていても出来上がるアプリは人によってかなり違ったものになります。例として、同期の@ruru8は社内で利用できる書籍サービスを作っていました。詳細は22日の投稿をお楽しみに。

研修期間中の過ごし方

  1. プログラムに沿って、アプリに機能を実装する
  2. 取り組んでいたステップの実装が終わる、または途中でもキリのいい単位で実装が行えたら、Githubの自分のリポジトリにローカルの内容をpushする
  3. 見て欲しいところにコメントを書いてプルリクエストを出し、メンターにレビューをお願いする
  4. LGTM(Looks Good To Me…レビューがOKであるという意味)がもらえれば次の実装に取り掛かる。修正が必要な場合は、ローカルで修正を行い、pushして再レビューをお願いする

という感じです。
わからないことがあったときは、会社で買っていただいている参考書籍を読むか、メンターや社内の先輩に質問します。参考書籍についてはこちらにまとめられています。
また、Classiはコミュニケーションツールとしてslackを使っておりまして、個人が分報チャンネルを持ち、困りごとや思ったことなどをつぶやく文化が活発です。分報についてはこちら。私も自分の分報である「times_ono」というチャンネルに研修の進捗を書くようにしておりまして、ここで質問するとチャンネルを見ていただいている先輩からよくアドバイスを頂けます。

image.png

これはtimes_onoの一幕でして、私がわからない!と言ったことに対して、同期の@hxrxchang君が席に助けに来てくれて、参考になるリンクも貼ってくれたところです。こんな感じで、気軽にSOSが出せて、助けてくれる人が社内にたくさんいる、すごく温かい環境です。

成果物

出来上がったアプリがこんな感じです。

image.png

私はレビューで時間をかけたため、ステップ4で考えていたコンセプトの実現は十分には行えず、シンプルに研修プログラムのステップを進めたアプリ、という感じの成果物になりました。ただ、私の場合はレビューで得た知識が評価され、チームで研修内容の共有会を開かせていただきました。

研修のゴールは人それぞれです。私はステップ23まで+研修内容の共有会の開催がゴールになりましたが、今研修中のベトナム人の新卒二人は1ヶ月で早くもステップ25を完了し、オプション要件に取り組んでいます。また、フロントエンドのスキルがあるメンバーはRailsのプログラミングと並行してAngularで画面の作成を行い、操作性に優れた格好いいアプリを作っています。Classiの先輩方に協力してもらい、ユーザビリティテストを行なったメンバーもいます。

研修を終えて

研修を通して、「プログラマーの仕事の大部分は、人への気遣いである」という感想を持ちました。なぜなら、メンターからの指摘事項のほとんどは、「他のエンジニアがアプリをメンテナンスする時に分かりやすい/後で困らないコーディング」についてのアドバイスだったためです。

  • こうした方が、他のプログラマーが分かりやすい!
  • この方がRailsや他のgemのアップデートに関わらず使える!
  • このコメントをつけた方が、レビュアーや他のエンジニアに親切!

などのコメントをたくさん頂くうちに、プログラマーの仕事像が「黙々と実装だけ行えれば良い、他の人とは関わらない」から、「他の人へ気遣い、情報を伝達し、理解して貰えるコードを書く」に変わっていきました。この意識は、現在チームで仕事をする上でも、チームメンバーへの気遣いや、伝わりやすいプルリクエストという形で活きています。

終わりに

この研修について社内の先輩エンジニアに話したところ、「羨ましい」という声を多く頂きました。2ヶ月以上かけてコードの書き方を教えてもらえる機会はとても貴重で、多くのプログラマーは仕事の中でコードの書き方やプルリクエストの仕方を覚えていくそうです。手厚い研修プログラムを組んでいただいたigaigaさん、ただあきさん、わからないところを教えて頂いた先輩エンジニアの皆さん、また充実した研修を受けさせて貰える会社の懐の広さに感謝しつつ、この投稿を締めます。

明日の投稿は@onigraさんです。お楽しみに。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CarrierWaveでpng画像を処理したときに色空間がGRAYになってしまってた

バージョン

  • ruby 2.6.1
  • Ruby on Rails 5.2.3
  • MacOS Catalina 10.15.1
  • ImageMagick 7.0.9-5
  • gem mini_magick 4.9.5
  • gem carrierwave 1.3.1

問題

CarrierWaveでMiniMagickを使って画像を処理する時。例えば、resize_to_fit とかresize_and_pad とか resize_to_fill とかを使うと、png画像の色空間が元々RGBだったのにGRAYになってしまう現象があった。

調査

どうやら、色空間をカラーで持っていても、グレイスケールっぽい画像だとImageMagickがカラースペースをグレイスケールに変換してしまうようだった。
なぜ・・、やめてくれ・・!

解決方法

上記した resize_to_fit とかのメソッドには、 combine_options という名前付き引数が渡せる。これはImageMagickのオプションと対応しているので、とにかく大量のものが指定できて、その中で、png画像での色の設定は、-defineオプションでpng:color-type=?を指定できる。
今回はRGBAで色情報を持ってほしかったので、png:color-type=6となる。

つまり、combine_options: {define: 'png:color-type=6'}を指定することで解決した。
Carrierwaveがどうとかいう問題ではなく、ImageMagick側の話だったようだ。

注意

これで解決したんだけど、このオプションは要注意なことが書いてあるので、png:color-typeの説明は見ておいた方が良さそう。

最初は色空間が変わるという現象だったから、combine_optionsに-colorspace sRGBを指定してたんだけど、これでは問題は解決できなかったのでちょっとハマった・・。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最近勉強して、知った言葉まとめてみた

はじめに

私が最近、知った言葉を、自分なりにまとめてみました。参考にしてもらえると、嬉しいです:relaxed:

ハッシュ化

パスワードを、保管する際に用いられる手法。
平文のパスワードからハッシュ値と呼ばれる値を求め、そのハッシュ値という値でパスワードを保管する手法。暗号化とは違い、不可逆変換であり、元の平文のパスワードに戻すことが不可能である。

データベースのNOTNULL制約

データベースに、nullを入れることを許容しない制約。制約をつけることにより、nullが入るとエラーが起きることにより、nullを入れることが出来なくなる。データベースの必須項目などに付けるといいと思います。

キーワード引数(Ruby)

関数を作った際の引数の末尾に : をつけることで、関数を呼び出す際にどの引数に、どの数や文字、変数を渡すのか指定することが出来る。引数が多くなった場合などに便利。

CSSセレクタ

CSSを指定することにより、要素を取得できる。多くのセレクタの種類があり、それらを組み合わせることにより、細かいとこまで指定して、要素を取得できる。おすすめチートシート

最後に

説明が曖昧ですが、もっと上手く説明できるように精進致します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】パンクズリストの作り方

まえがき

みなさんパンくずリストは知っていますでしょうか。どこかで耳にした方もいるかもしれません。ちょっと変わった名前ですがbreadcrumbsと書くとかっこいいですね。このパンくずリストですが名前によらずとても便利な物なのでぜひ作り方を覚えましょう。

パンクズリストとは

ホーム>おすすめ一覧>家電製品
のような現在いる位置を視覚的に見ることができる物です。大抵画面上部に用いられることが多いです。
これはよくSEO対策的に使われることが多く、SEOに詳しい人にとってはおなじみなのではないでしょうか。

パンクズリストを作るメリットは以下の通りですです。

設置するメリット

ユーザビリティーの向上

パンくずリストを用いることで視覚的に現在いる位置を構造的に見ることができるので、そのwebアプリを使うユーザーがサイト内で迷子になることがなくなり、ストレスなくサイト内巡回をすることができます。また、様々なユーザーはみなトップページからサイトに訪れるわけではなくそれぞれ必要なページに直接飛んでくるので、本当に目的のページを開けているか確認をするという使い方もされます。

検索ページに表示される時がある

googleなどで検索をかけた時にページの説明欄にパンクズリストが表示されているのを見たことがある人もいるかもしれません。パンくずが表示されていると検索ページにいるだけでサイト内の構造を見ることができ、飛びたいページの上層カテゴリも表示されるのでクリック率対策になります。

内部SEO対策

googleはページ内を評価する際に一つ一つのページに飛びリンクの文字などを認識しそのページが有用なのかどうかを判断しています。その際にパンクズリストがあるとその作業がスムーズになるために高く評価されSEO対策に効くとされています。

設置方法

では本題です。今回はRailsでの設置になります。
RailsではGretelというgemを用意してくれています。

GitHub
https://github.com/lassebunk/gretel

gem "gretel"

このようにgemfileの一番下に追加します。

$ bundle install

そしてコマンドを打ち、設定ファイルを作っていきます。

$ rails generate gretel:install

すると以下のようなファイルが生成されます。

config/breadcrumbs.rb
crumb :root do
  link "Home", root_path
end

# crumb :projects do
#   link "Projects", projects_path
# end

# crumb :project do |project|
#   link project.name, project_path(project)
#   parent :projects
# end

# crumb :project_issues do |project|
#   link "Issues", project_issues_path(project)
#   parent :project, project
# end

# crumb :issue do |issue|
#   link issue.title, issue_path(issue)
#   parent :project_issues, issue.project
# end

# If you want to split your breadcrumbs configuration over multiple files, you
# can create a folder named `config/breadcrumbs` and put your configuration
# files there. All *.rb files (e.g. `frontend.rb` or `products.rb`) in that
# folder are loaded and reloaded automatically when you change them, just like
# this file (`config/breadcrumbs.rb`).

今回はマイページを作っていきたいのでrootを設定した後にマイページを表記していきます。

config/breadcrumbs.rb
# ルート
crumb :root do
  link "ホーム", root_path
end

# マイページ
crumb :mypage do
  link "マイページ", mypage_users_path
end

crumbs :mypage doはなんという名前でhtml上に表記し呼び出すかを書きます、後々使います。
linkはパンクズリストに表示される文字とそのページがどこのパスに属しているかを表記します。
パンクズリストはリンクになっている場合が多いのでそのリンクはここで設定します。

ではビューファイルに表記していきます

mypage.html.haml
- breadcrumb :mypage
= breadcrumbs pretext: "You are here:",separator: " &rsaquo; "

- breadcrumb :mypageはconfig/breadcrumbs.rbに定義したmypageを呼び出すことができ、
= breadcrumbs pretext: "You are here:",separator: " &rsaquo; "
で表示したい位置を指定することができます。

&rsaquo;という表記はHTML特殊文字と言われる物での部分を表記しています。

また親子の関係を示すために以下のような表記方法もあります

profile.haml.haml
# プロフィール
crumb :profile do
  link "プロフィール", edit_user_path
  parent :mypage
end

parentと表記しdoとendで挟むことによりcrumb :profile doの親を書くことができます。
この表記だと次のような表示になります。

ホーム > マイページ > プロフィール

といった感じでしょうか。

まとめ

いかがだったでしょうか、今回書いて行ったパンクズリストは企業としても重宝する技術ですし、習得していて損はないのではないでしょうか。ぜひポートフォリオなどにも実装してみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyで「大石泉スキ」を標準出力する & idol++

この記事は 「大石泉すき」アドベントカレンダー 2日目の記事となります。
2日目は、1日目のPythonに対抗してRubyで標準出力してみます。

概要

以下のような方を対象にした内容になります。

  • 大石泉P
  • 大石泉に興味がある人
  • Rubyでプログラミングを始めたいと思っている人

なお、すそ野を広げるという観点にたって記事内のコードは以下の環境で実行しています。

OS: Windows 10
Rubyバージョン: ruby 2.6.3p62 (2019-04-16 revision 67580) [x64-mingw32]

とりあえず、Rubyで出力してみよう

初歩的な標準出力

Rubyがインストールされている状態でコマンドプロンプトを立ち上げて以下のコマンドを入力して実行します。
ruby -e "文字列""文字列" のコードをRubyが解釈して実行します。
いわゆるevalですね。

ruby -e "puts '大石泉スキ'"

大石泉_1_1.png

複数回出力する

ruby -e "5.times { puts '大石泉スキ' }"

大石泉_1_2.png

n.times { (処理) }n で指定した回数分だけ {} 内の処理を実行するループ構文です。
この場合は n = 5 を指定しているので5回文字列の出力が実行されます。

ランダム要素を加えてみよう

文字列を出力するだけでは面白味もないし、 idol++ ではないのでランダム要素を加えてみましょう。

ruby -e "puts ['大石泉スキ', 'idol++', 'ネイビーウェーブ',  'ビット・パフォーマー', 'ニューウェーブ・ネイビー', 'ニューウェーブ・バースデー'].sample"

大石泉_1_2.png

実行する毎に以下の要素のなかからランダムに出力されます。
Aray#sample を利用しています。

  • 大石泉スキ
  • idol++
  • ネイビーウェーブ
  • ビット・パフォーマー
  • ニューウェーブ・ネイビー
  • ニューウェーブ・バースデー

Rubyのclassファイルを作成してみよう

コマンドラインからの入力だけでは ビット・パフォーマー にはなれないのでRubyでclassを定義してコードを書いてみましょう。

初歩的なclass定義とコマンドラインからの実行

テキストエディタで以下のコードを入力して izumi.rb という名前で保存します。

class Izumi
  def perform
    puts '大石泉スキ'
  end
end

# コマンドラインから呼び出された時に実行される処理
izumi = Izumi.new
izumi.perform

コマンドプロンプトで以下のコマンドを入力して izumi.rb を実行します。

ruby izumi.rb

大石泉_2_1.png

irbからの実行

コマンドラインからの実行では1回限りの操作しか行えないので複数回の操作を繰り返して変化が起こるようにしてみましょう。
まずは、Rubyの対話型の実行環境である irb を立ち上げましょう。
--noreadline オプションをつけないと日本語入力が行えないので気を付けてください。

irb --noreadline

以下のようにRubyのコードを実行することができます。

大石泉_2_2_0.png

izumi.rb を以下のように書き換えます。

class Izumi
  def initialize
    @idol = 0
  end

  # Rubyはインクリメント演算子がないので全角文字を利用したメソッド名にしています
  def idol++
    @idol += 1
  end

  def perform
    puts '大石泉スキ'
    idol++

    if @idol % 3 == 0
      idol_rank = @idol / 3

      puts ''
      puts "大石泉のアイドルランクが上がった! => アイドルランク #{idol_rank}"
    end

    @idol
  end
end

izumi.rb ファイルのあるディレクトリに移動してirbを起動し、以下のコマンドを入力して izumi.rb の内容をirbに読み込みます。

require './izumi'

class定義から izumi インスタンスを作成して perform メソッドを呼び出します。

izumi = Izumi.new
izumi.perform

大石泉_2_2_1.png

perform メソッドを呼び出す毎に idol++ されて3回毎にアイドルランクがアップしていきます。

大石泉_2_2_2.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Active Storageを用いた複数ファイルアップロード機能の実装〜過程〜

今日のアウトプット

Railsから、
「Argument is too long」
というお叱りを受け、Active Storageでのファイルアップロード機能実装からの、Pythonプログラムにコントローラーでアップロードしたファイルを渡す部分を修正。

開発環境

Rails 5.2
Ruby 2.4
MySQL 14.14

ファイルアップロードの流れと実現したいこと

1,3つのファイルをViewからアップロードさせる。

new.html.erb
<div id="input-search">
  <%= form_with model: @context, local: true do |form| %>
    <%= form.file_field :files, multiple: true %>
    <%= form.submit "プログラムを実行する" %>
  <% end %>
</div>

2,inputされたファイルをDBに登録し、登録したファイルのフルパスを取得する。

contexts_controller.rb
class ContextsController < ApplicationController
  def new
    @context = Context.new
  end

  def create
    @context = Context.create context_params
    redirect_to context_path(@context.id, path_info)
    #→このように、paramsにそれぞれのファイルのフルパス が入ったarrayを渡して、showアクションに飛ばしたい。
  end

  def show
    @context = Context.find(params[:id])
    @file_result = ` python3 /~~/test.py "#{フルパス}" "#{フルパス}" "#{フルパス}" `
  end


  private
    def context_params
      params.require(:context).permit(files: [])
    end

    def params_info
      params[:context].permit!
    end
end

現状、どのような情報がViewからコントローラーへ渡っているのか?

View(new.html.erb)からControllerのcreateアクションに渡る際のparams内容↓

 Parameters: {"utf8"=>"✓", "authenticity_token"=>"L3qyctBHnYGUObv4Hgs/9+TuGhy+/Xpp2sCMQbTdI8l3uEM/sH+didAiDvATkr8exoH7ZE/aHNfm6OmUC2AHqw==", "context"=>{"files"=>[#<ActionDispatch::Http::UploadedFile:0x007fd74d3eb620 @tempfile=#<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1k0ld8p.txt>, @original_filename="testtest_1.txt", @content_type="text/plain", @headers="Content-Disposition: form-data; name=\"context[files][]\"; filename=\"testtest_1.txt\"\r\nContent-Type: text/plain\r\n">, #<ActionDispatch::Http::UploadedFile:0x007fd74d3eb5d0 @tempfile=#<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1jnpfkq.txt>, @original_filename="testtest_2.txt", @content_type="text/plain", @headers="Content-Disposition: form-data; name=\"context[files][]\"; filename=\"testtest_2.txt\"\r\nContent-Type: text/plain\r\n">, #<ActionDispatch::Http::UploadedFile:0x007fd74d3eb558 @tempfile=#<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1komhhu.txt>, @original_filename="testtest.txt", @content_type="text/plain", @headers="Content-Disposition: form-data; name=\"context[files][]\"; filename=\"testtest.txt\"\r\nContent-Type: text/plain\r\n">]}, "commit"=>"プログラムを実行する"}

ファイルを3つアップロードさせたので、paramsには、「ActionDispatch::Http::UploadedFile」クラスのオブジェクトが3つ生成されていることが確認できる。それぞれを分けて別々に記載すると、、

<ActionDispatch::Http::UploadedFile:0x007fd74d3eb620 @tempfile=#<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1k0ld8p.txt>, @original_filename="testtest_1.txt", @content_type="text/plain", @headers="Content-Disposition: form-data; name=\"context[files][]\"; filename=\"testtest_1.txt\"\r\nContent-Type: text/plain\r\n">
#<ActionDispatch::Http::UploadedFile:0x007fd74d3eb5d0 @tempfile=#<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1jnpfkq.txt>, @original_filename="testtest_2.txt", @content_type="text/plain", @headers="Content-Disposition: form-data; name=\"context[files][]\"; filename=\"testtest_2.txt\"\r\nContent-Type: text/plain\r\n">
#<ActionDispatch::Http::UploadedFile:0x007fd74d3eb558 @tempfile=#<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1komhhu.txt>, @original_filename="testtest.txt", @content_type="text/plain", @headers="Content-Disposition: form-data; name=\"context[files][]\"; filename=\"testtest.txt\"\r\nContent-Type: text/plain\r\n">]}

上記のようになっている。

3つのファイルのフルパス取得実現のために試したこと(コメントアウトして書いてあります)

その1

→上記で確認したparamsの内容から、@tempfileについての情報を抜き出せば良いと考えた。

ContectsController
class ContextsController < ApplicationController
  def new
    @context = Context.new
  end

  def create
    @context = Context.create context_params
  #その1 paramsからtempfileについての情報を抜き出して、配列化し、それをparamsに渡して利用する。
    path_info = []
    path_info.push(params[:context][:files][0].tempfile)
    path_info.push(params[:context][:files][1].tempfile)
    path_info.push(params[:context][:files][2].tempfile)
    redirect_to context_path(@context.id, path_info)
  end

  def show
    @context = Context.find(params[:id])
    @file_result = ` python3 /Users/~省略~/test.py "#{フルパス}" "#{フルパス}" "#{フルパス}" `
  end


  private
    def context_params
      params.require(:context).permit(files: [])
    end

    def params_info
      params[:context].permit!
    end
end

・createアクションで一度動作を止めて、path_infoに意図した情報が入っているか確認。

>> path_info
=> [#<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1gr9kjh.txt>, #<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1dw6zqz.txt>, #<Tempfile:/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191201-676-1j304y7.txt>]

それぞれのファイルのフルパスがしっかりと代入されているのがわかる。

・showアクションで一度動作を止めて、paramsにpath_infoに渡したフルパスが入っているかを確認。


<ActionController::Parameters {"controller"=>"contexts", "action"=>"show", "id"=>"92", "format"=>"#<File:0x007fd74e106558>/#<File:0x007fd74e948130>/#<File:0x007fd74e928f60>"} permitted: false>

formatとして、意図していない情報が入っている。フルパス を渡したはずなのに・・・
この時点で詰まりそうな予感がしている。

その2

params file read について
Ruby on Rails master@23b7382

→上記サイトからの情報を参考にし、「ActionDispatch::Http::UploadedFileのインスタンスに対して、readメソッドを使用することができる」とのことだったので、それぞれアップロードしたファイルの要素に対してreadメソッドを適用してみた。同様に、pathメソッドについても試して見た。

ソースコード(.readメソッド)

ContextsController
class ContextsController < ApplicationController
  def new
    @context = Context.new
  end

  def create
    @context = Context.create context_params

    path_info = []
    #その2 @temfileを用意し、paramsからそれぞれ情報を取り出して.readメソッドをかける。これについてはソースサイトがあったのだが、、
    @tempfile_1 = params[:context][:files][0].read
    @tempfile_2 = params[:context][:files][1].read
    @tempfile_3 = params[:context][:files][2].read
    path_info.push(@tempfile_1)
    path_info.push(@tempfile_2)
    path_info.push(@tempfile_3)
    #結果=> path_infoには何も代入されない。["","",""]となる

    redirect_to context_path(@context.id, path_info)
  end
def show
    @context = Context.find(params[:id])
    @file_result = ` python3 /Users/~省略~/test.py "#{フルパス}" "#{フルパス}" "#{フルパス}" `
  end


  private
    def context_params
      params.require(:context).permit(files: [])
    end

    def params_info
      params[:context].permit!
    end
end

・createアクションで一度動作を止めて、path_infoに意図した情報が入っているか確認。

>> path_info
=> ["", "", ""]

そもそもパスが取れていないので、これ以上は何もしない。

ソースコード(.pathメソッド)

ContextsController
class ContextsController < ApplicationController
  def new
    @context = Context.new
  end

  def create
    @context = Context.create context_params
    path_info = []
    @tempfile_1 = params[:context][:files][0].path
    @tempfile_2 = params[:context][:files][1].path
    @tempfile_3 = params[:context][:files][2].path
    path_info.push(@tempfile_1)
    path_info.push(@tempfile_2)
    path_info.push(@tempfile_3)
    redirect_to context_path(@context.id, context_params)
  end

def show
    @context = Context.find(params[:id])
    @file_result = ` python3 /Users/~省略~/test.py "#{フルパス}" "#{フルパス}" "#{フルパス}" `
  end


  private
    def context_params
      params.require(:context).permit(files: [])
    end

    def params_info
      params[:context].permit!
    end
end

・createアクションで一度動作を止めて、path_infoに意図した情報が入っているか確認。

>> path_info
=> ["/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191202-676-1y9ie46.txt", "/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191202-676-ppvmbd.txt", "/var/folders/9j/zkxb427x5zd1xjty84shsmk80000gn/T/RackMultipart20191202-676-15y9jdz.txt"]

pathメソッドがそれぞれのpathを取得してくれた。
これをparamsに絡めて、showアクションへと送れば良い!!

しかし、実際にやってみると、そんなURLはないぞ!とルーティングエラーが出てしまったので、そもそもparamsに絡める方法はダメなことがわかった。(今までなんとかparamsに絡めようとしていたのが馬鹿だったんだ)

しかし、pathメソッドを用いることで、それぞれのファイルのpathを得ることはできた。

ということはだ。paramsに絡めずに、createアクションで取得した取得した内容を、showアクションへ渡せばいい。

しかし、どうすれば良い??
考えついたのは以下の2パターン、これ等でいいのか?を吟味しよう。
・アクションの遷移に伴って、paramsには情報を含めずに、別の形でクラスインスタンスメソッド間でのやりとりをする方法を探す。
・クラスインスタンスメソッド(createアクション)が呼ばれた場合、欲しい値を取得するためのメソッドを新たに定義し、クラスインスタンスメソッド(showアクション)でこのメソッドを呼び出して、欲しい情報を取得する。
これ等について調べていこう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング未経験者向けにRubyワークショップをやった話

【島根県内学生限定】初心者向けプログラミング講座を実施する機会をもらったので、
そこで、やったこと、わかったこと、次にやることをまとめて残しておく(YWT)。

用意した題材

当日資料
ソースコード

やったこと(Y)

  1. プログラミングとはなにかを伝える
  2. チームで取り組む楽しさを伝える
  3. 参加者に次のステップを伝える

今回は題材として、Railsを使わない素の状態のRubyを使用。
島根県が推しているということもあるけど、県内にしっかりしたコミュニティがあること、学生の間は授業やイベントで触れる機会が多いことが大きな選定理由。

もし、職業としてのソフトウェアエンジニアに進むなら、遅かれ早かれ複数言語に触れることになるだろうし、県外に出ると「じゃあ、Rubyできるんでしょ?」と聞かれることもそれなりにあるので、触れたことがあるのは無駄にはならない(と思う)。

プログラミングの初期に学んでもらいたいこと様々あるけど、今回はプログラミングが目的達成のための道具という観点から、手ごろな課題に対して手持ちの武器(知識)をひねくり回して解決することを第一に。
ずっと以前に、勉強会で体験したDevnomiを下敷きにして「ポーカーの役判定プログラムをつくる」「みんなでひとつの画面をみながらキーを叩く人間を交代しながら」「用意されたテストコードをクリアする」というかたちで企画した。

コードを書くこと自体は、孤独にやらざるをえない場合も多いけど、同じようなレベルの人間が集まってわいわいやる楽しさを通して、学ぶことでレベルアップしていき、できることが増えるという正のフィードバックがあるということを体験してもらいたかった。

ちょっと前とはまた状況が変わったようで、情報関係の学生であっても自分のパソコンを持っていないことも多いので、無料のクラウドIDEPaiza CloudとGitHubを組み合わせることで、ワークショップ後も持ち帰って再現したり、続きに取り組めるようにすることで、エコシステムの一端に触れつつ、今後プログラミングに触れる際の足掛かりとしてもらうことを期待。

わかったこと(W)

  1. 講座資料にScrapBoxがとても便利
  2. モブプロはチーム学習として有効
  3. 未経験者には構造化の話がもっと必要

講座の資料作成は、ScrapBoxがとっても便利だった。全体の構成を粗くきめておきつつ、細部を思いつく都度作り込んでいくというやり方で資料作成をするのにぴったり。後からの推敲もとてもやりやすい。簡単なプレゼンテーション機能もついているので、資料=プレゼンテーションにもなり、最初にURLを共有すれば完全にペーパーレスで実施することができる。
加えてワークショップなので手を動かしてもらうシーンも多いのだけど、説明にスクリーンショットを追加したかったらGyazoと連携した画像の貼り付けで一発だし、シンタックスハイライトは完璧とはいわないけど、サンプルコードも組み込みやすく、臨機応変に資料を改変&共有するのには、ものすごく便利だった。

今回は初心者向けとターゲットを設定していたけど、次のイベントにつなげるという目的があるため、割と難しめのハードルをぶつけて、そこをチームで会話、協力しながら超えてほしいという期待を参加者にぶつけてみた。
これがうまくいった部分、いかなかった部分はあったけど、チームの中で議論しながらつくることを強制するやり方は、ある程度機能しており、事後アンケートでもチームだからついていけたという感想も得られた。学習については、とっかかりができればある程度ひとりでも進めることができるが、推進力=楽しさや好奇心というモチベーションは、体験や仲間がいることで得やすくなってくれると考えた。

とはいえ、サポートで各チームをまわっていると、比較的初歩の部分のとっかかりでつまづいたり、苦労していたので、導入部分のステップをもっと小刻みにしたり、例題を増やして理解度をサポートする小さな階段、をもっとつくるべきだったと痛感した。
参加者アンケートでは配列、ハッシュあたりから理解に苦労したという話もあったが、加えてチームで議論を進めるためには、メソッドの分割とか構造化のあたりの設計よりをもうちょっと丁寧にやらないといけなかったと感じた。

次にやること(T)

主に課題、資料のブラッシュアップの観点で、3つ。まだまだ改善の余地はいろいろある。

  1. 構造化の観点の追加
  2. シンプルなアルゴリズムの実装例題
  3. よりよく書く観点の追加

「この部分をプライベートメソッドとして切り出して」とか、モノシリックなメソッドではなく、ある程度構造化するという考え方について、今回思い切り抜けてしまっていたので、ここは本当に簡単な例題を用意するなり対策が必要だった。
また、初歩のアルゴリズムの例題もなにかあったほうがスムーズだった。カードの並び替えにしても、たしかにEnumerableのsort使えば確かに一発なんだけど、いったんべったべたな実装でバブルソートを実装してみて、それを少しずつ改善して読みやすくして、最終的にはこの便利メソッド使って、と漸進的に進化させることを体感してもらったほうがよかった。
外のワークショップでもほとんどの場合、やりたいことから逆算して、必要なものを調べながら学ぶという方法をとっているのだけど、未経験者向けにやるということから、もっと足がかりとなるとなる材料が必要だった。これは、当日その場で出したような直接的なサンプルより、もっと抽象度をあげたアルゴリズムの形で例題を事前にこなすよう、1日目のインプット時間の使い方を変えた方がよかった。

参考サイト、当日持ち込んだ書籍等

イラスト素材

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む