- 投稿日:2021-10-31T23:11:04+09:00
WEBRickでWebサーバを立ち上げて同一LAN内のパソコンからアクセスする
動機 フレームワーク無しでもWEBRickで簡単にWebサーバが作れるというのを読んで、面白そうだったので手元で試してみます。以下、忘れないようにするための記録です。 WEBRickでWebサーバーを立ち上げる(まずはローカル専用) WEBRickについて詳しいことはこちらを見つつ。 ↑これを見ると、 WEBrick は Ruby 3.0 で標準ライブラリから削除されました。Ruby 3.0 以降で WEBrick を使いたい場合は rubygems から利用してください。 とのことなので、gem installしておきます。 $ gem install webrick 次に、適当なディレクトリを作ってファイルを作成します。 server.rb require 'webrick' options = { BindAddress: '127.0.0.1', Port: '10080', DocumentRoot: '.' } server = WEBRick::HTTPServer.new(options) trap(:INT) { server.shutdown } # Signal.trap(:INT) { server.shutdown } でも同じ server.start Webサーバオブジェクト(つまりWEBRick::HTTPServerのインスタンス)はstartメソッドを持ち、これが最終的にWebサーバを起動させます。 Webサーバオブジェクトは、WEBRick::HTTPServer.new(options)で作成しています。 optionsとしてはとりあえずBindAddress, Port, DocumentRootの3項目を渡せば動くそうです。他の項目についてはるりまにまとまっていました。 BindAddressに127.0.0.1を渡せばローカル専用のWebサーバになり、0.0.0.0を渡せばLAN内に公開されたWebサーバになります。 trap(:INT)の行で、ctrl+cが入力された時の動作を指定しています。ここではserver.shutdownでサーバーを止めています。(この行を書いていないとctrl+cで止められません。ps aux | grep webrickでプロセスidを調べてkillするとか) あとはこのファイルを実行すればWebサーバが立ち上がります。 $ ruby server.rb #[2021-10-31 18:09:13] INFO WEBrick 1.7.0 #[2021-10-31 18:09:13] INFO ruby 3.0.2 (2021-07-07) [x86_64-darwin20] #[2021-10-31 18:09:13] INFO WEBrick::HTTPServer#start: pid=23294 port=10080 curlでアクセスしてみます。 $ curl http://127.0.0.1:10080 # (以下結果) <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <HTML> <HEAD> <TITLE>Index of /</TITLE> <style type="text/css"> <!-- .name, .mtime { text-align: left; } .size { text-align: right; } td { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } table { border-collapse: collapse; } tr th { border-bottom: 2px groove; } //--> </style> </HEAD> <BODY> <H1>Index of /</H1> <TABLE width="100%"><THEAD><TR> <TH class="name"><A HREF="?N=D">Name</A></TH><TH class="mtime"><A HREF="?M=D">Last modified</A></TH><TH class="size"><A HREF="?S=D">Size</A></TH> </TR></THEAD> <TBODY> <TR><TD class="name"><A HREF="..">Parent Directory</A></TD><TD class="mtime">2021/10/31 18:19</TD><TD class="size">-</TD></TR> <TR><TD class="name"><A HREF="webrick227.rb">webrick227.rb</A></TD><TD class="mtime">2021/10/31 18:19</TD><TD class="size">375</TD></TR> </TBODY></TABLE><HR> <ADDRESS> WEBrick/1.7.0 (Ruby/3.0.2/2021-07-07)<BR> at 127.0.0.1:10080 </ADDRESS> </BODY> </HTML> Indexページのhtmlが返ってきました! curlで指定したのと同じurlをブラウザで開くと...あれ? 10080番ポートはブラウザで見られない ERR_UNSAFE_PORTと書いてあります。調べるとどうやら、ブラウザが最近10080番ポートのサイトを見られないようにしているようです。 素直に10080番を避けて、optionsのPortに指定するのを20080とかにしてみます。 server.rb # 省略 options = { BindAddress: '127.0.0.1', Port: '20080', DocumentRoot: '.' } # 省略 urlは http://127.0.0.1:20080 になります。 するとこんな画面になりました。 ↓ optionsのDocumentRootで指定したディレクトリにindex.htmlというファイルがなければ、この画面になります。(ホームページにアクセスすると「index of/」と表示され、ファイルが一覧で表示されてしまう) 今回はDocumentRootにはカレントディレクトリ('.')を指定しているので、server.rbと同じ階層にindex.htmlを作成して中身を適当に用意してみます。 index.html <h1>Hello World!</h1> ブラウザでさっきと同じurl( http://127.0.0.1:20080 )にアクセスすると... 先ほどのIndex of /という画面ではなく、index.htmlの中身が表示されるようになりました! WebサーバをLAN内に公開する 上で書いたように、optionsのBindAddressに127.0.0.1を渡せばローカル専用のWebサーバになり、0.0.0.0を渡せばLAN内に公開されたWebサーバになります。 (カフェなどの公共のLANに繋いでる時は危険なので試さないほうがいいと思います) server.rb # 省略 options = { BindAddress: '0.0.0.0', Port: '20080', DocumentRoot: '.' } server = WEBRick::HTTPServer.new(options) # 省略 もう一度サーバを立ち上げ直します。 $ ruby server.rb これで、同一LAN内の他のパソコンやスマホからでもアクセスできるようになっているはずです。 アクセスするには127.0.0.1の代わりに、プライベートIPアドレスを使います。もちろんポート番号(ここでは20080)も引き続き付ける必要があります。 urlは例えばhttp://192.168.1.21:20080のようになるはずです。 これで、同じWi-Fiに繋いだ他の端末のブラウザにこのurlを打ち込むと、index.htmlの内容が見られました。 遊びでいろいろ使えそうな感じです。 繰り返しになりますが、公共のLANに繋いでる時は試さないほうがいいと思います。
- 投稿日:2021-10-31T23:08:37+09:00
Selenium + ChromeDriver + Ruby によるスクレイピング環境を Docker で構築する。
はじめに Docker コンテナを定期実行する形で Selenium によるスクレイピングをしたいことがあったのでその時のメモ書き。 Dockerfile Selenium を動かすにはブラウザ(Chrome)とブラウザのドライバー(ChromeDriver)が必要なのでインストールします。 # 執筆時点での最新 FROM ruby:3.0.2 # Chrome をインストール RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add \ && echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list \ && apt-get update -qq \ && apt-get install -y google-chrome-stable libnss3 libgconf-2-4 # ChromeDriver をインストール # 現在の最新のバージョンを取得し、それをインストールする。 RUN CHROMEDRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` \ && curl -sS -o /tmp/chromedriver_linux64.zip http://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip \ && unzip /tmp/chromedriver_linux64.zip \ && mv chromedriver /usr/local/bin/ ## 下記以降はよしなに # スクレイピング用の Ruby ソースコードをコピー(任意) # Gemfile では `selenium-webdriver` をインストールしている。 WORKDIR /app COPY Gemfile /app COPY Gemfile.lock /app RUN bundle install COPY . /app # スクレイピング用の Ruby ソースコードを実行(任意) CMD ["bundle", "exec", "ruby", "bin/crawler.rb"] Ruby のサンプルコード Yahoo Japan のサイトからh1タグのテキストを取ってくるだけのコード。 crawler.rb require "selenium-webdriver" options = Selenium::WebDriver::Chrome::Options.new # 下記オプションをつけないと. Docker上では動かなかった。 options.add_argument('--headless') options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') driver = Selenium::WebDriver.for :chrome, options: options driver.navigate.to "https://www.yahoo.co.jp/" h1 = driver.find_element(:css, "h1") puts h1.text driver.quit おわりに ヘッドレスモードなどのオプションをつけないとエラーで動かなかったり、 Chromeのバージョンは自動で最新がインストールされるので、ChromeDriverもバージョンを合わせないといけないのですが、そのあたりうまく解決できました。 Amazon Batch で Selenium を動かすためにどうしても Docker が使いたかったのですが、 結構簡単に構築できてよかったです。
- 投稿日:2021-10-31T21:45:37+09:00
【Ruby silver】&, &&, |, ||演算子それぞれを使ったコードについて考える
次のコードを実行するとどうなりますか a = [1, 2, 5, 7, 8] b = [1, 3, 6, 7, 9] c = nil || a & b | a && a | b p c 演算子の優先順位とそれぞれの演算子について理解していないと解けない問題。 積集合「&」 重複している要素は取り除かれ、共通する要素を取り出す。 a = [1, 2, 3] b = [1, 3, 5] a & b #=> [1, 3] &&演算子 &&は a と b がどちらも真のとき、最後に評価された b を出力する a && b #=> [1, 3, 5] 和集合「|」 重複している要素は取り除かれ、重複している要素をまとめて出力する。 a = [1, 2, 3] b = [1, 3, 5] a | b #=> [1, 2, 3, 5] ||演算子 ||は a と b がどちらかが真のとき、最初に評価された a を出力する a = [1, 2, 3] b = [1, 3, 5] a || b #=> [1, 2, 3] a = nilの場合右辺の処理が行われる a || b #=> [1, 3, 5] 上記の知識を踏まえて考えてみる a = [1, 2, 5, 7, 8] b = [1, 3, 6, 7, 9] c = nil || a & b | a && a | b p c ⑴優先順位を考える 演算子の優先順位 高い & | && || 低い ⑵優先順位の順番に処理を考える 1番初めに評価するもの【積集合(共通している要素を取り出す)】 c = nil || a & b | a && a | b a & b の場合は共通する要素を取り出す=> [1, 7] 2番目に評価するもの【和集合(重複している要素を全て取り出す)】 c = nil || ①(a & b | a) && ②(a | b) c = nil || [1, 7] | a && a | b ①[1, 7] | a => [1, 7] | [1, 2, 5, 7, 8] => [1, 7, 2, 5, 8] ②a|b => [1, 2, 5, 7, 8, 3, 6, 9] 3番目に評価するもの【&&演算子】 c = nil || a & b | a && a | b [1, 3, 7, 2, 5, 8] | [1, 2, 5, 7, 8, 3, 6, 9] どちらも真の場合最後に評価された値を返す => [1, 2, 5, 7, 8, 3, 6, 9] 4番目に評価するもの【||演算子】 nil || [1, 2, 5, 7, 8, 3, 6, 9] 左辺がnilの場合は右辺の処理が行われる 回答 [1, 2, 5, 7, 8, 3, 6, 9] 感想 難しい! 参考資料:
- 投稿日:2021-10-31T20:02:15+09:00
putsメソッド、printメソッド、pメソッドの違い
メソッド 出力後の改行 メソッド自体の戻り値 改行文字を含めた場合 配列を出力した場合 呼び出すメソッド puts 改行を加えて出力する。 nil 改行される 配列の[]を含まず各要素ごとに改行される to_s print 改行を加えない。 nil 改行される 配列の[]を含め改行なし to_s p 改行を加えて出力する。 ただし文字列を出力するとその文字列がダブルクオートで囲まれる。 引数のオブジェクトそのものが改行されずに/nのまま表示される 配列の[]を含め改行なし inspect putsメソッドとprintメソッドは内部的に引数で渡されたオブジェクトに対してto_sメソッドを呼び出して文字列に変換している。 pメソッドはto_sメソッドではなくinspectメソッドを呼び出す。 文字列が出力後にダブルクオート付きになっていた理由として内部的にStringクラスのinspectメソッドが呼び出され値が出力されている。 inspectメソッドは開発者にとって役立つ情報が含まれている。 違いについて実際に検証した結果? モジュール名が実行結果として出力。 対象がクラスの場合、#<クラス名:オブジェクトid インスタンス変数>が実行結果として出力。 クラスについてto_sメソッドと比べると inspectメソッドではクラス名とオブジェクトidとインスタンス変数を出力、 to_sメソッドはクラス名とオブジェクトidのみ出力しインスタンス変数は出力しない。 配列・ハッシュともに値がそのままの形で表示される。 文字列に関してto_sと比べるとダブルクオートで囲まれているか・いないかの違いがある。 nilに関してはto_sで出力した場合は実行結果として表示されない。
- 投稿日:2021-10-31T19:47:25+09:00
【RSpec】Railsチュートリアル第6版 第8章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章 第5章 第6章 第7章 第8章(本記事) 事前準備 Gemfileにテストデータ作成用のFactory Botを追加 Gemfile group :development, :test do gem "factory_bot_rails" end Minitest Sessionsコントローラのテスト test/controllers/sessions_controller_test.rb require 'test_helper' class SessionsControllerTest < ActionDispatch::IntegrationTest test "should get new" do get login_path assert_response :success end end ユーザーログイン処理に対するテスト test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "login with valid email/invalid password" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: @user.email, password: "invalid" } } assert_not is_logged_in? assert_template 'sessions/new' assert_not flash.empty? get root_path assert flash.empty? end test "login with valid information followed by logout" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert is_logged_in? assert_redirected_to @user follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) delete logout_path assert_not is_logged_in? assert_redirected_to root_url follow_redirect! assert_select "a[href=?]", login_path assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 end end ユーザーログインのテストで使うfixture test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> テスト中のログインステータスを論理値で返すメソッド test/test_helper.rb ENV['RAILS_ENV'] ||= 'test' . . . class ActiveSupport::TestCase fixtures :all def is_logged_in? !session[:user_id].nil? end end RSpec Sessionsコントローラのテスト spec/requests/sessions_spec.rb require 'rails_helper' RSpec.describe "Sessions", type: :request do describe "GET /new" do it "returns http success" do get login_path expect(response).to have_http_status(:success) end end end ユーザーログイン処理に対するテスト spec/system/users_login_spec.rb require 'rails_helper' RSpec.describe "UsersLogins", type: :system do before do @user = FactoryBot.create(:user) end it "login with valid email/invalid password" do visit login_path expect(page).to have_current_path "/login" fill_in "Email", with: @user.email fill_in "Password", with: "invalid" click_button "Log in" expect(page).to have_current_path "/login" expect(page).to have_content("Invalid email/password combination") visit root_path expect(page).to_not have_content("Invalid email/password combination") end it "login with valid information followed by logout" do visit login_path expect(page).to have_current_path "/login" fill_in "Email", with: "michael@example.com" fill_in "Password", with: "password" click_button "Log in" expect(page).to have_current_path "/users/#{@user.id}" expect(page).to_not have_css("a", text: "Log in") expect(page).to have_css("a", text: "Log out") expect(page).to have_css("a", text: "Profile") click_on "Log out" expect(page).to have_current_path "/" expect(page).to have_css("a", text: "Log in") expect(page).to_not have_css("a", text: "Log out") expect(page).to_not have_css("a", text: "Profile") end end systemスペックにおいて、sesssion機能は使用できないらしいので、is_logged_in?メソッドはrequest specにて別途記述する。 expect(page).to have_current_path "/"でエラーが発生する。 ユーザーログインのテストで使うfactory spec/factories/users.rb FactoryBot.define do factory :user do name {"Michael Example"} email {"michael@example.com"} password {"password"} password_confirmation {"password"} end end テスト中のログインステータスを論理値で返すメソッド spec/support/support.rb module Support def is_logged_in? !session[:user_id].nil? end end RSpec.configure do |config| config.include Support end spec/supportディレクトリにサポートモジュールを作成し、is_logged_in?を追加。RSpec.configureでサポートモジュールをincludeする。 is_logged_in? spec/requests/session_spec.rb require 'rails_helper' RSpec.describe "session test", type: :request do before do @user = FactoryBot.create(:user) end it "log in and log out" do post login_path, params: { email: "michael@example.com", password: "password", } expect(response).to redirect_to user_path(@user) expect(is_logged_in?).to be_truthy delete logout_path expect(response).to redirect_to root_path expect(is_logged_in?).to be_falsey end end expect(response).to redirect_to user_path(@user)でエラーが発生する。 発生するエラーについて ユーザーログイン処理に対するテストの中のexpect(page).to have_current_path "/"のテストにおいて、 Failure/Error: expect(page).to have_current_path "/" expected "/logout" to equal "/"というエラーが発生する。 click_button "Log in"の時と違い、ルーティンの時点あるいはSessionsコントローラーがうまく動作していないようである。 click_buttonとclick_onとの動作の違いや、Bootstrapのドロップダウン機能の影響ではないことは確認済み。 is_logged_in?の中のexpect(response).to redirect_to user_path(@user)のテストにおいて、 Failure/Error: expect(response).to redirect_to user_path(@user) Expected response to be a <3XX: redirect>, but was a <422: Unprocessable Entity> というエラーが発生する。 どなたか解決策があれば是非教えていただきたい。
- 投稿日:2021-10-31T19:22:07+09:00
【Ruby silver】範囲式 フリップフロップ演算子
次のコードを実行するとどうなりますか。 10.times{|d| print d == 3..d == 5 ? "T" : "F" } 解説文 d == 3..d == 5の部分は条件式に範囲式を記述しています。 この式は、フリップフロップ回路のように一時的に真偽を保持するような挙動をとります。 詳細は、Rubyリファレンスに詳しい説明がありますのでそちらを参照してください。 初めて見た時に混乱しました。 どうゆう順番で評価するか整理して考える必要があったのでまとめます。 ⑴Rangeの復習 # ..の場合 a = (1..5).to_a => [1, 2, 3, 4, 5] # ...の場合 a = (1...5).to_a => [1, 2, 3, 4] ? ..の場合は5を含む1〜5の範囲になる。 ...の場合は4.9999...までは対象になるが5は範囲に含まない。整数で考えると1〜4が範囲になる。 ひとつずつ崩して考えて理解する 10.times {|d| print d == 3..d == 5 ? "T" : "F" } 10.times 0..9の範囲を考える => 0123456789 (d == 3)..(d == 5) dが3〜5の時に(わかりやすく括弧で囲いました。) ? "T" : "F" 条件式:trueの場合"T"を出力、falseの場合は"F"を出力。 【補足】 三項演算子: 条件式 A ? 式 B : 式 C 条件式 A が真の時は式 B が実行され、偽の時は式 C が実行される。 0123456789 345がTrueのためTが出力される 結果としてFFFTTTFFFFが出力される ソニックガーデンjnchitoさんの記事: その他参考解説:
- 投稿日:2021-10-31T14:17:49+09:00
Progate Ruby on Rails5 VI ~ VII 個人的ざっくりまとめ
emailの重複がないかチェック バリデーションでuniqueness: trueを指定 models/user.rb class User < ApplicationRecord validates :email, {uniqueness: true} end マイグレーションファイルのみ作成 データベースに変更を加えるが、モデルは必要ないとき rails g migration ファイル名をターミナルに入力 ターミナル $ rails g migration add_image_name_to_users マイグレーションファイルの仕組み テーブルの変更を加えるには、マイグレーションファイルのchangeメソッドの中に処理をかく 20170427053118_add_image_name_to_users.rb class AddImageNameToUsers < ActiveRecord::Migration[5.0] def change # 変更内容 end end カラムを追加するマイグレーションファイル add_column テーブル名, カラム名, データ型をchangeメソッドの中にかく 20170427053118_add_image_name_to_users.rb class AddImageNameToUsers < ActiveRecord::Migration[5.0] def change add_column :users, :image_name, :string end end ※rails db:migrateはこのchangeメソッドの中身を実行するためのコマンド ターミナル $ rails db:migrate # changeメソッドの中身を実行 画像の送信 画像の送信の場合はform_tag("URL", {multipart: true})と指定する必要がある users/edit.html.erb <%= form_tag("/users/#{@user.id}/update"), {multipart: true}) do %> <p>画像</p> <input name="image" type="file"> <% end %> Rubyのコードでファイルを作成 File.write(ファイルの場所, ファイルの中身)と指定する ターミナル $ rails console > File.write("public/sample.txt", "Hello World") 画像の保存 画像を保存するためには、画像データを元に画像ファイルを作成する必要がある ↓ ファイルを作成するにはFileクラスを使う ↓ File.binwrite(ファイルの場所, ファイルの中身(画像データ))と指定する ※readメソッドを用いることで、その画像データを取得することができる users_controller.rb def update : : @user.image_name = "#{@user.id}.jpg" image = params[:image] File.binwrite("public/user_images/#{@user.image_name}", image.read) end
- 投稿日:2021-10-31T09:48:58+09:00
【コードリーディング】Rails 1.0 ActiveRecordのソースコードを読んでみた
はじめに Railsポートフォリオ作成でActiveRecordを使っており、作成当時は正直内部処理がどうなっているのか分かっていませんでし、気にもしていませんでした笑 そういった中、実務案件で直接SQLを書く機会があり、ActiveRecordって実際どういう風にSQLに置換して実行しているのか気になりはじめました。 そこで、ActiveRecordのソースコードでSQLに置き換えている仕組みの部分のコードを読んで理解した部分を書き留めたいと思います。 今回読んだソースコード 今回はコードの流れをシンプルに見たかったので最新のソースコードではなくRails v1.0.0のソースコードを読みました。とは言ってもコードは約1800行もあり、読むのになかなか骨が折れました笑 その中でもController部分でよく使用するfindメソッドに絞って読んだものを記載します。 ソースコード元はこちら そもそもActiveRecordって? ActiveRecordはRailsにおいてデータベースからデータを取り出したり、データベースにデータを保存したりする役割(MVCでいうMのModelの部分)を担っています。 ActiveRecordのメリットは主に以下の2点かなと感じております。 ・簡易的な記述でSQL文を発行できる ・MySQL、PostgreSQLなど様々なデータベースに対し共通の記述で対応することが可能 O/Rマッパーと呼ばれるデータベースとプログラムを橋渡しする役目を担うものがActiveRecordに含まれているため、多様なデータベースからデータを取り出したり、データベースにデータを保存したりといった処理がスムーズに行うことができます。 早速コードを読んでいく active_record/base.rb/find def find(*args) options = extract_options_from_args!(args) #省略 case args.first when :first find(:all, options.merge(options[:include] ? { } : { :limit => 1 })).first when :all records = options[:include] ? find_with_associations(options) : find_by_sql(construct_finder_sql(options)) records.each { |record| record.readonly! } if options[:readonly] records else return args.first if args.first.kind_of?(Array) && args.first.empty? expects_array = args.first.kind_of?(Array) conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] ids = args.flatten.compact.uniq case ids.size when 0 raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}" when 1 if result = find(:first, options.merge({ :conditions => "#{table_name}.#{primary_key} = #{sanitize(ids.first)}#{conditions}" })) return expects_array ? [ result ] : result else raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}" end else ids_list = ids.map { |id| sanitize(id) }.join(',') result = find(:all, options.merge({ :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"})) if result.size == ids.size return result else raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}" end end end end 最初の部分はallやfirstなど、find内に記載した引数に対し条件分岐するコードが記載されていました。 はじめにextract_options!で可変長引数(柔軟な引数指定)を受け取れるメソッドを使い引数を受け取りoptionsへ代入 :all、:firstを引数としており、:allの中でさらに分岐が行われている模様。 ・:firstの場合 inclued(モジュールを呼び出す)されなければ、optionsに{ :limit => 1 }を代入し、:allへ id指定の場合はconditionsに"#{table_name}.#{primary_key} = #{id}"を追加し:allへ ・:allでやっていること :includeオプションありの場合find_with_associations(options)へ :includeオプションなしの場合find_by_sql(construct_finder_sql(options))へ 今回はfind_by_sql(construct_finder_sql(options))を読み進めていきたいと思います。 find_by_sqlメソッド まずはfind_by_sql(construct_finder_sql(options))のfind_by_sqlメソッドから active_record/base.rb/find_by_sql(sql) def find_by_sql(sql) connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } end find_byは今でも残る重要なメソッドですよね!なんだか知ってるメソッドがあるだけで嬉しい気持ちになりました笑 find_byメソッドはid以外のカラムを検索条件としたい場合に使用しますが、まさにカラムをとってきて返している処理が書かれています。 ここで気になったのはsanitize_sqlとinstantiate sanitize_sqlメソッド 公式ドキュメントはこちら これによるとサニタイズした値を入れることによって意図しないSQLの挙動を防ぐことができるみたいですね。このメソッドはSQLインジェクション攻撃対策のために使用されています。 サニタイズってなに? 特別な意味を持つ文字の特別さを無効化する処理のこと SQLインジェクションとは? SQLインジェクションは、Webアプリケーションのパラメータを操作してデータベースクエリに影響を与えることを目的とした攻撃手法です。SQLインジェクションは、認証をバイパスする目的でよく使われます。他にも、データを操作したり任意のデータを読み出したりする目的にも使われます。 Rails セキュリティガイド - Railsガイド 不正な「SQL」の命令を注入し、意図しないSQL文を発行し攻撃すること。 例えばDELETE文を発行され、データを全て消されてしまうなど対策をしないとセキュリティ的に非常に危険。 instantiate instantiateは読み進めているとメソッドとして定義している箇所がありました。 active_record/base.rb/instantiate def instantiate(record) object = if subclass_name = record[inheritance_column] if subclass_name.empty? allocate else require_association_class(subclass_name) begin compute_type(subclass_name).allocate rescue NameError raise SubclassNotFound, "The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + "Please rename this column if you didn't intend it to be used for storing the inheritance class " + "or overwrite #{self.to_s}.inheritance_column to use another column for that information." end end else allocate end object.instance_variable_set("@attributes", record) object end ほぼ読む必要はないがallocateによって取得したものが新しいレコードかを判断するメソッドと理解しました。レコードに保存されていないカラムや値の場合はエラーを吐き出す仕様になっている模様。 find_by_sqlメソッド総括 総括してfind_by_sqlメソッドはサニタイズした値を入れ込む事によりSQLインジェクション攻撃を防ぎつつ、取ってきたレコードを返す。レコードはinstantiateにより既存のレコードかNewレコード化を判断し、場合によりエラーを吐き出す仕様になっていると言う感じですかね。 construct_finder_sqlメソッド 続いてfind_by_sql(construct_finder_sql(options))のconstruct_finder_sqlメソッドについて読み進めました。 active_record/base.rb/construct_finder_sql def construct_finder_sql(options) sql = "SELECT #{options[:select] || '*'} FROM #{table_name} " add_joins!(sql, options) add_conditions!(sql, options[:conditions]) sql << " GROUP BY #{options[:group]} " if options[:group] sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options) sql end ここは結構SQL文そのままって印象でした。用途によっていろんなSQL文を発行しテーブルからデータを参照している印象です。 保存、更新 最後にfindとは関係なくなってしまいますがActiveRecordによる保存、更新処理部分を読み進めて終わりたいと思います。 saveメソッド active_record/base.rb/save def save raise ActiveRecord::ReadOnlyRecord if readonly? create_or_update end saveメソッドによりcreateやupdateを呼び出しています。 createメソッド active_record/base.rb/save def create if self.id.nil? and connection.prefetch_primary_key?(self.class.table_name) self.id = connection.next_sequence_value(self.class.sequence_name) end self.id = connection.insert( "INSERT INTO #{self.class.table_name} " + "(#{quoted_column_names.join(', ')}) " + "VALUES(#{attributes_with_quotes.values.join(', ')})", "#{self.class.name} Create", self.class.primary_key, self.id, self.class.sequence_name ) @new_record = false end 愚直にテーブル名、カラム名、プライマリーキー、idなどをSQLで挿入していますね。 updateメソッド active_record/base.rb/update def update(id, attributes) if id.is_a?(Array) idx = -1 id.collect { |id| idx += 1; update(id, attributes[idx]) } else object = find(id) object.update_attributes(attributes) object end end ここでidから検索しidとattributes(Modelの属性全て)を渡して更新しています。 検証中に失敗した場合はオブジェクトを返しています。 attributesについてはこちらが参考になりました。 あとがき 今回は初めてOSS(オープンソースソフトウェア)を読んでみました。 ソースコードを辿ってみた結果愚直にSQLを発行していたと言うことがわかりました。 自分で作成したポートフォリオの比じゃないコード量を読み進めていくのは大変でしたが一つ一つメソッドを辿ってく内に理解していく感覚がすごく楽しかったです。 企業の自社サービスのコードはこれの比じゃないくらい膨大な記述量になっているとは思いますが、早くコードを読んでみたいと言う願望が高まっています。 これからも挙動の根幹をコードを読み進めて理解していきたいと思います。 最後までご覧いただきありがとうございました!!
- 投稿日:2021-10-31T08:22:28+09:00
【Ruby】条件分岐 備忘録
目的 条件分岐 備忘録 結論 とりあえず3種類あった。 if文 unless文 case文 timesメソッド #if文 a = 10 b = 20 if a > b puts "aはbより大きくない" elsif a < b puts "aはbよりも小さい" end =>aはbより大きくない timesメソッド #unless文 a = 10 b = 20 unless a > b puts "aはbより大きくない" end =>aはbより大きくない timesメソッド #case文 array = [ "a", 1, nil ] array.each do |item| case item when String puts "item is a String" when Numeric puts "item is a Numeric" else puts "item is something" end end =>item is a String item is a Numeric item is a something 参考 たのしいRuby第5版
- 投稿日:2021-10-31T04:07:32+09:00
【RSpec】Railsチュートリアル第6版 第7章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章 第5章 第6章 第7章(本記事) Minitest ユーザー登録に対するテスト test/integration/users_signup_test.rb require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, params: { user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' end test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! assert_template 'users/show' end end RSpec ユーザー登録に対するテスト spec/system/users_signup_spec.rb require 'rails_helper' RSpec.describe "UsersSignups", type: :system do it "invalid signup information" do visit root_path click_on "Sign up" expect { fill_in "Name", with: "" fill_in "Email", with: "user@invalid" fill_in "Password", with: "foo" fill_in "Confirmation", with: "bar" click_button "Create my account" }.to_not change(User, :count) expect(page).to have_current_path "/users" expect(page).to have_content("Name can't be blank") expect(page).to have_content("Email is invalid") expect(page).to have_content("Password confirmation doesn't match Password") expect(page).to have_content("Password is too short (minimum is 6 characters)") end it "valid signup information" do visit root_path click_on "Sign up" expect { fill_in "Name", with: "Example User" fill_in "Email", with: "user@example.com" fill_in "Password", with: "password" fill_in "Confirmation", with: "password" click_button "Create my account" }.to change(User, :count).by(1) expect(page).to have_current_path "/users/#{User.last.id}" expect(page).to have_content("Welcome to the Sample App!") end end click_onでユーザー登録ページに移動し、fill_inでフォームに情報を入力した後、click_buttonで情報を送信する。 expect(page).to have_current_pathで正しいページに移動しているかチェックし、expect(page).to have_contentでエラーメッセージとフラッシュがページに存在しているかチェック。
- 投稿日:2021-10-31T03:39:51+09:00
【RSpec】Railsチュートリアル第6版 第6章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章 第5章 第6章(本記事) 第7章 Minitest Userテスト test/models/user_test.rb require 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar") end test "should be valid" do assert @user.valid? end test "name should be present" do @user.name = "" assert_not @user.valid? end test "email should be present" do @user.email = " " assert_not @user.valid? end test "name should not be too long" do @user.name = "a" * 51 assert_not @user.valid? end test "email should not be too long" do @user.email = "a" * 244 + "@example.com" assert_not @user.valid? end test "email validation should accept valid addresses" do valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn] valid_addresses.each do |valid_address| @user.email = valid_address assert @user.valid?, "#{valid_address.inspect} should be valid" end end test "email validation should reject invalid addresses" do invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com] invalid_addresses.each do |invalid_address| @user.email = invalid_address assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" end end test "email addresses should be unique" do duplicate_user = @user.dup duplicate_user.email = @user.email.upcase @user.save assert_not duplicate_user.valid? end test "password should be present (nonblank)" do @user.password = @user.password_confirmation = " " * 6 assert_not @user.valid? end test "password should have a minimum length" do @user.password = @user.password_confirmation = "a" * 5 assert_not @user.valid? end end RSpec Userテスト spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do before do @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar") end it "should be valid" do expect(@user).to be_valid end it "name should be present" do @user.name = "" @user.valid? expect(@user.errors[:name]).to include("can't be blank") end it "email should be present" do @user.email = "" @user.valid? expect(@user.errors[:email]).to include("can't be blank") end it "name should not be too long" do @user.name = "a"*51 @user.valid? expect(@user.errors[:name]).to include("is too long (maximum is 50 characters)") end it "email should not be too long" do @user.email = "a" * 244 + "@example.com" @user.valid? expect(@user.errors[:email]).to include("is too long (maximum is 255 characters)") end it "email validation should accept valid addresses" do valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn] valid_addresses.each do |valid_address| @user.email = valid_address expect(@user).to be_valid end end it "email validation should reject invalid addresses" do invalid_addresses = %w[user@example,com user_at_foo.org user.name@example. foo@bar_baz.com foo@bar+baz.com] invalid_addresses.each do |invalid_address| @user.email = invalid_address @user.valid? expect(@user.errors[:email]).to include("is invalid") end end it "email addresses should be unique" do duplicate_user = @user.dup duplicate_user.email = @user.email.upcase @user.save duplicate_user.valid? expect(duplicate_user.errors[:email]).to include("has already been taken") end it "password should be present (nonblank)" do @user.password = @user.password_confirmation = " " * 6 @user.valid? expect(@user.errors[:password]).to include("can't be blank") end it "password should have a minimum length" do @user.password = @user.password_confirmation = "a" * 5 @user.valid? expect(@user.errors[:password]).to include("is too short (minimum is 6 characters)") end end model specが自動で作成されていなければrails g rspec:model userで作成。 expect(@user.errors[:?]).to include("?")でエラーメッセージをテスト。
- 投稿日:2021-10-31T02:20:44+09:00
【RSpec】Railsチュートリアル第6版 第5章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章 第5章(本記事) 第6章 第7章 事前準備 Gemfileにsystem spec用のcapybaraを追加 Gemfile group :test do gem 'capybara' end ファイル内容の変更 spec/support/capybara.rb RSpec.configure do |config| config.before(:each, type: :system) do driven_by :rack_test end config.before(:each, type: :system, js: true) do driven_by :selenium_chrome_headless end end Minitest StaticPagesコントローラーのテスト test/controllers/static_pages_controller_test.rb require 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest def setup @base_title = "Ruby on Rails Tutorial Sample App" end test "should get home" do get root_path assert_response :success assert_select "title", "Ruby on Rails Tutorial Sample App" end test "should get help" do get help_path assert_response :success assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end test "should get about" do get about_path assert_response :success assert_select "title", "About | Ruby on Rails Tutorial Sample App" end test "should get contact" do get contact_path assert_response :success assert_select "title", "Contact | Ruby on Rails Tutorial Sample App" end end レイアウトのリンクに対するテスト test/integration/site_layout_test.rb require 'test_helper' class SiteLayoutTest < ActionDispatch::IntegrationTest test "layout links" do get root_path assert_template 'static_pages/home' assert_select "a[href=?]", root_path, count: 2 assert_select "a[href=?]", help_path assert_select "a[href=?]", about_path assert_select "a[href=?]", contact_path end end RSpec StaticPagesコントローラーのテスト spec/requests/static_pages.rb require 'rails_helper' RSpec.describe "StaticPages", type: :request do let(:base_title) { 'Ruby on Rails Tutorial Sample App' } describe "GET root" do it "returns http success" do get root_path expect(response).to have_http_status(:success) assert_select "title", "#{base_title}" end end describe "GET /help" do it "returns http success" do get help_path expect(response).to have_http_status(:success) assert_select "title", "Help | #{base_title}" end end describe "GET /about" do it "returns http success" do get about_path expect(response).to have_http_status(:success) assert_select "title", "About | #{base_title}" end end describe "GET /contact" do it "returns http success" do get contact_path expect(response).to have_http_status(:success) assert_select "title", "Contact | #{base_title}" end end end Contactページのテストを追加 名前付きルートを使用するよう変更 レイアウトのリンクに対するテスト spec/system/site_layout_spec.rb require 'rails_helper' RSpec.describe "SiteLayouts", type: :system do it "layout links" do visit root_path expect(page).to have_current_path "/" expect(page).to have_link nil, href: root_path, count: 2 expect(page).to have_link 'Help', href: help_path expect(page).to have_link 'About', href: about_path end end system specを作成し、レイアウトの各リンクをテスト。
- 投稿日:2021-10-31T02:10:34+09:00
【RSpec】Railsチュートリアル第6版 第4章
はじめに Railsチュートリアル第6版のテストをRSpecで書き直していく。 目次 第3章 第4章(本記事) 第5章 第6章 第7章 Minitest StaticPagesコントローラーのテスト test/controllers/static_pages_controller_test.rb require 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest def setup @base_title = "Ruby on Rails Tutorial Sample App" end test "should get home" do static_pages_home_url assert_response :success assert_select "title", "Ruby on Rails Tutorial Sample App" end test "should get help" do static_pages_help_url assert_response :success assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end test "should get about" do static_pages_about_url assert_response :success assert_select "title", "About | Ruby on Rails Tutorial Sample App" end end RSpec StaticPagesコントローラーのテスト spec/requests/static_pages.rb require 'rails_helper' RSpec.describe "StaticPages", type: :request do let(:base_title) { 'Ruby on Rails Tutorial Sample App' } describe "GET /home" do it "returns http success" do get "/home" expect(response).to have_http_status(:success) assert_select "title", "#{base_title}" end end describe "GET /help" do it "returns http success" do get "/help" expect(response).to have_http_status(:success) assert_select "title", "Help | #{base_title}" end end describe "GET /about" do it "returns http success" do get "/about" expect(response).to have_http_status(:success) assert_select "title", "About | #{base_title}" end end end /homeで基本タイトルのみを表示するよう変更。