20210414のRubyに関する記事は20件です。

【Rails】コードレビューで脆弱性を発見しよう!セキュリティコードレビューのルール10選

コードレビューは誰かが書いたソースコードを確認することで、ソフトウェアの問題を発見する作業です。 コードレビューで発見できる問題には、コーディング規約違反や機能的なバグだけでなく、セキュリティの欠陥、つまり脆弱性も含まれます。 本記事では筆者が実際に脆弱性を発見したことがあるコードレビューのルールを10個紹介します。 1. JSON からの情報漏洩 SPAなどAjaxによる画面更新を実装するために JSON を返すアクションを作ることがあります。その際、オブジェクトを不用意にシリアライズしてしまうと意図しない情報漏洩につながります。 レビュールール コントローラを正規表現 render.+?json で検索し、必要最小限の属性のみシリアライズされていることを確認します。 悪い例 オブジェクトをそのまま render render :json => @user user に氏名住所など非公開情報や、パスワードのような秘密情報が含まれていたら大変ですね。 良い例 画面描写に必要な属性だけシリアライズ render json: @user.slice(:id, :first_name) 補足 jb や jbuilder を使っている場合は、テンプレートに不要な属性が含まれていないことを確認してください。 2. Strong Parameters が適切に設定されていること Strong Parameters で更新可能なパラメータを不用意に permit してしまうと、開発者が意図しないデータを変更される危険性があります。 レビュールール すべてのコントローラを文字列 .permit で検索し、不要な属性が含まれていないことを確認します。 悪い例 全ての属性を permit params.require(:user).permit! 不要な属性を permit params .require(:user) .permit(:email, :admin, :first_name, :last_name) admin (管理者フラグ) を変更できてしまうのは危険です。 良い例 必要最小限の属性だけ permit params .require(:user) .permit(:email, :first_name, :last_name) 3. クエリに認可が含まれていること クエリに認可が含まれていない場合、URL等に含まれるIDが改ざんされて他のユーザのリソースが不正に操作されてしまうことがあります。 例)自分のプロフィール画面のURL http://example.com/customers/123 の 123 を 444 に変えてみたら、他人のプロフィールが見えてしまった。 レビュールール コントローラを正規表現 params\[:.*id\] で検索し、リソースにアクセス制御が必要な場合、アクションまたはクエリにアクセス制御が含まれているかを確認します。 悪い例 全てのリソースを更新できてしまう def update @article = Articles.find(params[:article_id]) @article.update!(article_params) redirect_to @article end article_id パラメータを変更することで、他人の article も更新できてしまいます。 ただし、下記の場合は安全です。 コントローラ、アクションでアクセス制御を実装している(例えば管理者のみ使用できる、など) cancancan や pundit でアクセス制御を実装している ※アクセス制御ロジックの妥当性は別途確認しましょう 良い例 クエリにアクセス制御が含まれている def update @article = @current_user.articles.find(params[:article_id]) @article.update!(article_params) redirect_to @article end article_id パラメータを変更しても、ユーザ自身の article しか参照・更新できないので安全です。 4. CORS が適切に構成されていること CORSの設定ミスは Same Origin Policy によるセキュリティを緩めてしまい、意図しない情報漏洩につながる可能性があります。 レビュールール すべてのコードを Access-Control-Allow-Origin で検索し、信頼できない Origin を設定していないことを確認します。 悪い例 ワイルドカード headers['Access-Control-Allow-Origin'] = '*' リクエストヘッダをコピー headers['Access-Control-Allow-Origin'] = request.headers["Origin"] null headers['Access-Control-Allow-Origin'] = 'null' ※非公開情報を含まない、オープンなAPIの場合のみ * を設定しても安全です。 良い例 信頼できる Origin headers['Access-Control-Allow-Origin'] = `our-website.example.com' 許可リストに基づいて検証された request.headers["Origin"] if %w(example.com example.jp).include?(request.headers["Origin"]) headers['Access-Control-Allow-Origin'] = request.headers["Origin"] end 補足 凝ったCORS設定が必要な場合はrack-corsを使う場合もありますが、その場合も Access-Control-Allow-Origin の妥当性は確認しましょう。 CORSについての詳細はMDN: オリジン間リソース共有 (CORS)を参照してください。 5. レースコンディション:ファイルパスが重複しないこと 一時ファイルなどを固定のファイルパスに書き込みしている場合、同時に複数のリクエストが実行されるとデータが壊れてしまいます。これによって整合性がとれなくなったり、意図しない情報漏洩につながったしります。 レビュールール すべてのコードを正規表現 File.open\(.+?"w で検索し、ファイルパスが重複する可能性がないことを確認します。 悪い例 固定値 f = File.open("tmp/upload-file", "w") ユーザがアップロードしたファイルの名前 file = fileupload_param[:file] output_path = Rails.root.join('public', file.original_filename) f = File.open(output_path, "w") ※該当するロジックがアクションから呼び出されない場合は安全です。 良い例 重複する可能性がないか超低い output_path = Rails.root.join('tmp', SecureRandom.uuid) File.open(output_path, "w") { |f| 補足 そもそも一時ファイルを作るだけなら tempfile を使いましょう。 6. レースコンディション:時刻をIDにしていないこと ファイルだけでなく、IDやキーも同様に重複するとレースコンディションが発生する可能性があります。 過去に見受けられたケースとして、時刻をIDとして使ってしまうケースがありました。利用者が少数の時は発現する可能性は低いですが、利用者が増えてくるとリクエストが同時に処理される可能性が高くなり、複数のリクエスト間で同一のリソースを操作してしまいます。これによってデータの整合性がとれなくなったり、意図しない情報漏洩につながったしります。 レビュールール すべてのコードを正規表現 Time\.(now|current|zone\.now) で検索し、IDやファイル名などを現在時刻だけで作成していないかを確認します。 悪い例 IDが時刻のみで構成されている File.open("tmp/#{Time.zone.now.to_i}", "w") ※概要するロジックがアクションから呼び出されない場合は安全です 良い例 IDが時刻+ユニークな値で構成されている File.open("tmp/#{Time.zone.now.to_i}_#{SecureRandom.uuid}", "w") 補足 IDとなりうるものとして、ファイルやデータベースのレコードだけでなく、S3のオブジェクトキーなども含まれます。 7. CSRF対策:GETメソッドでリソースを変更していないこと GETメソッドのアクションでリソースの状態を変更すると、CSRF攻撃に対して脆弱になります。 レビュールール GETメソッドでリソースを変更していないことを確認します。どのアクションがGETメソッドなのかは rails routes の出力を見ると良いでしょう。 悪い例 GETメソッドのアクションでデータを作成、更新、削除をしている場合、CSRF攻撃に脆弱です。 # GET /articles/:id/change_to_private def change_to_private @article = @current_user.articles.find(params[:id]) @article.private = true ... 良い例 GETメソッドのアクション内がデータの参照のみの場合、安全です。 def show @article = @current_user.articles.find(params[:id]) end 8. CSRF対策:トークン検証を省略していないこと CSRFトークンの検証を安易に無効化すべきではありません。 過去の事例として、Ajax通信をすると Can't verify CSRF token authenticity エラーが出るからと言って検証を無効化しているケースがありました。 レビュールール コントローラを次の文字列で検索し、トークン検証を無効化していないことを確認します。 verify_authenticity_token skip_forgery_protection protect_from_forgery 悪い例 変更が伴うアクションでCSRFトークン検証を省略すべきではありません。 class HogeController < ApplicationController protect_from_forgery :except => [:index, :show, :update] class HogeController < ApplicationController skip_forgery_protection class HogeController < ApplicationController skip_before_action :verify_authenticity_token 良い例 参照系のアクションだけど、クエリ文字列が長くなってしまうためにHTTPメソッドをPOSTに変更しているような場合、CSRFトークン検証を省略しても安全です。 class HogeController < ApplicationController protect_from_forgery :except => [:search] セッションに基づかないリクエスト(API等)の場合、CSRFトークン検証を省略しても安全です。 class HogeApiController < ApplicationController skip_forgery_protection 補足 Rails の CSRF 対策については、Rails セキュリティガイド:CSRFへの対応策 を参照してください。 9. 標準のセキュリティ機能を無効化していないこと Rails 標準のセキュリティ機能を無効化すべきではありません。 レビュールール config/application.rb の各種設定を確認し、設定値の妥当性を確認します。 注意が必要な例 CSRFトークン検証を無効化 config.action_controller.allow_forgery_protection = false デフォルトのセキュリティヘッダーを削除 config.action_dispatch.default_headers.clear デフォルト値やヘッダの意味はRailsガイドの9 デフォルトのヘッダーを参照してください。 10. Ransack を ransackable なしで使ってないこと 便利すぎる機能は、時として脆弱性となってしまうこともあります。ここでは ransack を使った検索機能を例に選びました。 ransack は高機能な検索機能を実装するのに便利ですが、非公開の属性で検索出来てしまうと、検索結果の差異から非公開の値を特定できてしまいます。これは暗号化されたパスワードやトークンなど、データベースに保存された秘密情報の漏洩につながります。 レビュールール Gemfileに ransack が含まれている場合、ソースコードを ransack で検索します。下記3つの条件を満たす場合、脆弱です。 {モデル}.ransack のパラメータがユーザ入力 モデルにパスワードやトークン等、非公開の属性が含まれている ransackable_attribute, ransackable_associations, ransortable_attributes がモデルに定義されていない、または非公開の属性が含まれている ちょっと複雑ですが、要は非公開の属性やアソシエーションで検索できたらNGということです。 悪い例 1. ransack のパラメータがユーザ入力 # app/controllers/users_controller.rb def search @q = User.ransack(params[:q]) 2. モデルに非公開の属性が含まれている # db/schema.db create_table "users", force: :cascade do |t| t.string "email" t.string "password" t.string "api_key" t.boolean "admin" t.string "first_name" t.string "last_name" t.datetime "created_at" t.datetime "updated_at" end どの属性が非公開かはアプリケーションの仕様によりますが、少なくとも password や api_key は公開してはいけない属性でしょう。 3. ransackable_attribute, ransackable_associations, ransortable_attributes がモデルに定義されていない、または非公開の属性が含まれている # app/models/user.rb class User < ApplicationRecord # def ransackable_* がない end 良い例 次の 1, 2, 3 いずれかに当てはまる場合、安全です。 1. ransack のパラメータがユーザ入力ではない Article.ransack(id_eq: 1) 2. モデルに非公開の属性やアソシエーションが含まれていない # db/schema.db create_table "users", force: :cascade do |t| t.string "email" t.string "first_name" t.string "last_name" t.datetime "created_at" t.datetime "updated_at" end 3. ransackable_attribute, ransackable_associations, ransortable_attributes がモデルに定義されており、必要な属性・アソシエーションに絞られている # app/models/user.rb class User < ApplicationRecord # ... def self.ransackable_attributes(auth_object = nil) super & %w(email first_name last_name) end def self.ransortable_attributes(auth_object = nil) ransackable_attributes(auth_object) end def self.ransackable_associations(auth_object = nil) [] end # ... end 補足 ransack の認可 ransackable の詳細は Ransack: Authorization (whitelisting/blacklisting) を参照してください。 ransack で検索してもマッチしない場合、search を使っている可能性があります。詳細は Ransack #search method を参照してください。 Next step 脆弱性を発見するコードレビュールールを10個紹介しました。こんな感じでコードレビューをすると脆弱性を発見できるかもしれません。 次に、よりセキュアにアプリケーション開発をするためにできることを書いておきます。 Brakeman 今回はソースコードを目視で確認しましたが、自動で解析してくれるツールもあります。 Brakeman は、SQLインジェクションやクロスサイトスクリプティングなどインジェクション系の脆弱性やセキュリティ設定の不備をそれなりの精度で検出してくれる便利なツールです。 ただし Brakeman は全ての脆弱性を見つけてくれるわけではありません。本記事で紹介した脆弱性のほとんどは Brakeman では検出できません。 そのため、実際のコードレビューでは Brakeman と目視によるレビューの両方で相互補完するのが効果的です。 Ruby on Rails セキュリティガイド 脆弱性がコードレビューで見つかるのはとても良いことですが、そもそも作りこまないことも重要です。脆弱性を作りこみにくい、安全にコーディングすることをセキュアコーディングといいます。 Rails セキュリティガイド にはWebアプリケーションの脆弱性とRailsにおける対策方法が満載です。セキュアコーディングの実践に大いに役に立つでしょう。 セキュリティコードレビュールールの改善 本記事で紹介したルールはあくまで例なので、アプリケーションによっては検索にマッチしすぎたり、逆に見つけられなかったりすることもあります。 Rails セキュリティガイドを参考にしたり、過去に発覚した脆弱性をもとにルールを追加・変更していきましょう。 本記事のルールを改善する良いアイデアがあったらぜひ教えてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails herokuのエラー:Failed to install gems via Bundler. の対応

ポートフォリオ作成の際に、詰まったのでメモ bundle installしても解決しない場合です。 環境 ruby 2.6.6 Rails 6.0.3 経緯 $ heroku create 後に $ git push heroku main そこでエラー発生 . . remote: ! remote: ! Failed to install gems via Bundler. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed . . 解決方法 エラー文を読むと、bundlerに問題あるとのこと heroku公式サイトからbundlerの記事を検索してみる。 Applications specifying Bundler 2.x in their Gemfile.lock will receive bundler: 2.2.15 Applications specifying Bundler 1.x in their Gemfile.lock will receive bundler: 1.17.3 Applications with no BUNDLED WITH in their Gemfile.lock will default to bundler: 1.17.3 https://devcenter.heroku.com/articles/ruby-support#supported-runtimes 2021年 4月現在 今のbundlerのversionはいくつかな? $ bundler -v Bundler version 2.x.xx ふむ、この場合 Applications specifying Bundler 2.x in their Gemfile.lock will receive bundler: 2.2.15 とあるので、2.2.15にしないといけないらしい。 bundlerのversionを設定し直す まずアンインストール $ gem uninstall bundler version指定して再インストール $ gem install bundler --version '2.2.15' gemfile.lockを消去 (あとでbundle installして最新のものを作成) $ rm gemfile.lock version 2.2.15で、bundle install $ bundle _2.2.15_ install git add, commit して再デプロイ $ git push heroku 通りました。 これでも、解消しない場合、エラー文をよく読むと . . remote: is x86_64-linux. Add the current platform to the lockfile with `bundle lock remote: --add-platform x86_64-linux` and try again. . . とあり、 $ bundle lock --add-platform x86_64-linux で解決することもあるそうです。 ついで $ heroku create NAME NAME 部分に好き文字を付けることができますが、付けない場合はランダムになります。 もし後からつけたい場合、 $ heroku app:rename NAME で簡単に書き換えられます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【At Corder】【初心者】ABC087B - Coins をRuby で解いてみた

はじめに AtCoder に登録したら次にやること ~ これだけ解けば十分闘える!過去問精選 10 問 ~ - Qiita こちらの記事を参考に初心者がAt corderに挑戦します。 目的としては、就職活動でのコーディングテスト対策です。 毎日1問を目標としてコツコツやってきます わからないことは調べる精神です。ちょっとでもわからないなぁ、と思ったことは調べて解説と参考文献を載せますので、同じ内容でわからない人がいれば参考にししてください 問題 ABC087B - Coins 500円玉をA枚、100円玉をB枚、50円玉をC枚ある。これらの中から何枚か選び、合計金額をちょうどX円にする方法は何通りあるか? 同じ種類の硬貨同士は区別できない。2通りの硬貨の選び方は、ある種類の硬貨についてその効果を選ぶ枚数が異なるときに区別される。 制約 0≤A,B,C≤50 A+B+C≥1A+B+C≥1 50≤X≤20,000 A,B,C は整数である Xは50の倍数である 入力 出力 入力 A B C X 出力 硬貨を選ぶ方法の個数を出力せよ 入力例 2 2 2 100 出力例 2 500 円玉を 0 枚、 100 円玉を 1 枚、 50 円玉を 0 枚選ぶ。 500 円玉を 0 枚、 100 円玉を 0 枚、 50 円玉を 2 枚選ぶ。 回答 a = gets.to_i b = gets.to_i c = gets.to_i x = gets.to_i count = 0 #入力 for i in 0..a #500円玉の枚数 x_1 = x - 500*i break if x_1 < 0 #マイナスだとループを抜ける for j in 0..b #100円玉の枚数 x_2 = x_1 - 100*j break if x_2 < 0 for k in 0..c #50円玉の枚数 x_3 = x_2 - 50*k count += 1 if x_3 == 0 break if x_3 <= 0 end end end puts count 解説 500円玉から評価するようにしました。そうすることで、マイナスになる処理の回数を減らせるかと思いました。 変数が多くなってしまった気がするのですが、配列を使うなどして変数を減らしたほうが良いコードでしょうか?(良いコードという表現があっているのか謎ですが、、) 感想 今回はメソッドの仕様以外はなにも参照せずにコードを書くことが出来ました。 目的は達成できましたが、まだまだブサイクな所が多いように感じます。笑 もっとコード数を減らせる方法がないか模索していきたいですね。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails 環境構築メモ

背景 久しぶりにRailsの環境構築したので自分用のメモです。 基本の流れ ・下記記事を参考に進めさせて頂きました。 【完全版】MacでRails環境構築する手順の全て - Qiita ※Bundlerは2.010以前をインストールする(bundler 2.0.1以降をインストールしているとYou must use Bundler 2 or greater with this lockfile.という厄介なエラーに遭遇することが多々あるため) ・Bundlerのバージョンを変更する場合 Ruby | bundler を特定のバージョンに切り替えて実行する - Qiita ・rbenvでインストールするrubyのバージョンを変更する場合 rbenvでrubyのバージョンを管理する - Qiita エラー解決 Gemをインストール時 undefined method `invoke_with_build_args' for nil:NilClass というエラーが出る場合 ・下記記事を参考に解決。 Yosemiteに変えたらgem installできなくなった件 - Qiita ・opensslをうまく参照できていないため上記のようなエラーが発生する。 上記記事内でrbenvを再インストールするよう指示があるが、opensslを参照できていない場合opensslのインストールで止まってしまう。その場合は下記記事を参考にパスを貼り直す必要がある。 rbenv installがopensslで失敗する - Qiita ・rbenvのインストールは遅いため最大で10分以上かかる可能性がある。 全然進まなくてもエラーではないため注意 rbenv install 遅くて痺れを切らした話 - Qiita DBにMySQLを選択し、bundle install時 ld: library not found for -lssl , Make sure that gem install mysql2 -v 〇〇 というエラーが出る場合 ・下記記事を参照 bundle installでmysql2がエラーになる件 - Qiita サーバー起動時に、Please run rails webpacker:install Error: No such file or directory @ rb_sysopenというエラーが出る場合 ・webpackerがインストールできていないのが原因 yarnにpathを通し、bundle exec rails webpacker:installすることで解決 サーバー起動時に、Unknown database 'pet_reserve_development'というエラーが出る場合 ・そもそもDBが存在しないのが原因 MySQLクライアントで作成するよりrails db:createで作成するほうがラク 以上になります。記事を書いてくださった皆さんありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails初学者によるRailsチュートリアル学習記録② 第0章

目次 1. はじめに 2. これまでの学習内容 3. Railsチュートリアルを読了して 1. はじめに この記事は、Rails初学者の工業大学三年生がRailsチュートリアルの学習記録をつけるための記事です。 筆者自体がRailsやWebについて知識が少ないので、内容の解釈などに間違いがある可能性があります。(その時はコメントで指摘してくださると助かります!) 2. これまでの学習内容 前回の記事に引き続き、Railsチュートリアルの第8章から第14章の内容を、 キーワードレベルで見返せるように記録しておきます。 ▶第8章 -ログイン機能を作成する- cookiesを使ってセッション機能を作成する ログインフォームを作成する フォームへの入力値からユーザを検索する ログインの成功、失敗に応じてメッセージを表示する ログアウト機能を作成する ▶第9章 -ユーザのログイン情報を記憶する- 記憶トークンを生成し暗号化する 記憶しているユーザ情報を削除する ▶第10章 -ユーザの更新・表示・削除- ユーザ情報の更新 情報更新時のバリデーション 更新の成功、失敗に応じた処理 ユーザが可能な操作の管理 ログインしてないユーザが実行できる操作の設定 ログインしているユーザが実行できる操作の設定 ユーザを表示する ユーザ一覧のページネーション ユーザを削除する ▶第11章 -新規登録時にアカウントの有効化を要求する- ユーザの初期状態を有効化されていない設定をする ヘルパー(組み込み・カスタム) ユーザ登録時にメールを送信する メイラーの使い方 ▶第12章 -パスワードを再設定できるようにする- パスワード再設定のメールを送信できるようにする データベース内のパスワードを更新する アセットパイプラインとは 名前付きルートの使い方 統合テストの作り方 ▶第13章 -メッセージの投稿機能の作成- 投稿内容のバリデーション 投稿とユーザの関連付け 投稿の表示 画像の投稿 ▶第14章 -ユーザーのフォロー機能の作成- フォローしている、フォローされているという関係をデータモデルで表現する 関係性をユーザと関連付ける フォローしているユーザの投稿を表示させる 3. Railsチュートリアルを読了して 学習の流れとして、まず一度読んでから実際に手を動かすという流れを考えていたが、 一通り読んだだけでもこの教材のボリュームを実感できました。 特にこの記事でメモした8章以降の内容は実装する機能がより複雑な物となっており、 データモデルの考え方やテストといったprogateなどの教材では学ぶことのなかった内容は、 読んでいてほとんど理解することができませんでした。 これから実際に手を動かして学習を進めていきますが、学習スピードを保ちながら進められるか非常に不安です:(
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTMLタグにRubyのコードを含める方法(hoge.nameがhoge.timeに投稿しました)

結論 HTMLタグの後に、 "#{@hoge.username}が#{@hoge.display_created_at}に投稿" と書く。 ポイント ●""で囲う ●さらに入れ後で#{}でRubyのコードを入れる ●DBのカラムではなく、メソッドを作成した方が好ましい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスト

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

Rubyで特殊フォルダのパスを取得する方法

前書き Windowsには特殊フォルダとして知られる「既知のフォルダ」がいくつかあり そのパスは環境によってはデフォルトとは別であることもあります。 例: Program Filesやユーザーディレクトリやデスクトップなど これらを取得する方法としてSHGetKnownFolderPathがあります。 ソース specialdir.rb require 'fiddle/import' require 'fiddle/types' module Shell32 extend Fiddle::Importer dlload 'shell32.dll', 'Ole32.dll' include Fiddle::Win32Types # HRESULT SHGetKnownFolderPath( # REFKNOWNFOLDERID rfid, # DWORD dwFlags, # HANDLE hToken, # PWSTR *ppszPath # ); extern "DWORD SHGetKnownFolderPath(PDWORD id, DWORD flgs, HANDLE token, char** path)" extern "void CoTaskMemFree(void* pv)" module_function def wcscpy!(s) buf = [] 0.step(by: 2) do |i| a,b = s[i], s[i+1] c = a | b << 8 break if c == 0 buf.concat([a, b]) end buf.pack("C*").force_encoding("UTF-16LE") end module_function def GetKnownFolderPath(id, flg = 0) ppstr = Fiddle::Pointr.new.ref ret = SHGetKnownFolderPath(id, flg, 0, ppstr) fail ArgumentError, "SHGetKnownFolderPath return E_INVALIDARG" if ret == 0x80070057 # E_INVALIDARG fail "SHGetKnownFolderPath return 0x#{ret.to_s(16)}" if ret != 0 str = ppstr.ptr # 返ってきたppstrから文字列を取り出しCoTaskMemFreeで解放 wcscpy!(str).encode("UTF-8").tap{ CoTaskMemFree(str) } end end 例えばLocalLowを取得する場合は locallow.rb # {A520A1A4-1780-4FF6-BD18-167343C5AF16} FOLDERID_LocalAppDataLow = [0xA520A1A4, 0x1780, 0x4FF6, 0xBD, 0x18, 0x16, 0x73, 0x43, 0xC5, 0xAF, 0x16].pack("L<S<S<C8") puts locallow = Shell32.GetKnownFolderPath(FOLDERID_LocalAppDataLow) # => C:\Users\{ユーザー}\AppData\LocalLow 反省点 wcscpy!にエラー落ち・無限ループの危険がある。(そもそもこの実装は原始的すぎてなんとも・・・) モジュール名がShell32なのにOle32のapiも呼び出している。 windows以外の場合にエラー・警告を出した方が親切 というか、外部モジュールにした方が100万倍楽
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rubyでweb apiを使ってみよう(ハンズオン)

はじめに この記事は, rubyでweb apiの使いかたを理解するための初心者向けの記事です。具体的には, rubyで郵便APIを使用し, コンソールに表示するまでを行います。 バージョン : ruby3.0.0 WebAPIとは何か WebAPIの概説 みなさん、WebAPIって何かわかりますか!? シーン.... おっと(笑)。でも大丈夫ですよ、丁寧に説明していこうと思います。厳密な定義などが知りたい方は, 記事に記載されている公式リファレンスを参照してくださいね。 まず、APIについて見ていきましょう。Mozzilaの公式リファレンスの説明によるとAPIは次のように説明されています。 Application Programming Interfaces (APIs) は、開発者が複雑な機能をより簡単に作成できるよう、プログラミング言語から提供される構造です。 平たく言うと, 複雑なコードで書かれた難しいプログラムを, 誰でも簡単に扱えるようにしたものです。 そして, このAPIのうちサーバからデータ取得をする APIのことをWebAPIと呼びます。 例えば、WebAPIでは,TwitterAPIがありますが、これは最新のツイートをwebサイトに表示するという機能を簡単に扱えるようにしてあるAPIです。他にも,GoogleAPIやFacebook API、YouTube APIなど様々なAPIが提供されています。 WebAPIを使ってみよう 流れ それでは、実際にWebAPIを使っていきましょう。 流れとしては、 ①uriモジュールを使って, URIをパースする (URIの解析) ②net/httpモジュールを使って, APIにget通信でAPIのサーバにアクセスして、サーバからJSON形式の結果をもらう。(HTTP通信でAPIにアクセス) ③jsonモジュールを使って, JSON形式をhash形式に変換する (JSONの結果をHashに変換する) ④digメソッドでhashから値を取り出す(Hashから値を取り出す) 郵便APIの構成 ここで試しに使う郵便APIは,以下のとおりです。?以降のzipcode=〇〇に郵便番号を指定する事で住所を返してくれます。ブラウザでこのコードにアクセスすると, APIにアクセスした結果が表示されます。 http://zipcloud.ibsnet.co.jp/api/search?zipcode=2500011 実装(ハンズオン) 最後にコードで実装すると次のようになります require 'net/http' require 'uri' require 'json' #①uriモジュールを使って, URIをパースする (URIの解析) uri = URI.parse('http://zipcloud.ibsnet.co.jp/api/search?zipcode=2500011') #②net/httpモジュールを使って, APIにget通信でAPIのサーバにアクセスして、サーバからJSON形式の結果をもらう。(HTTP通信でAPIにアクセス) json = Net::HTTP.get(uri) #③jsonモジュールを使って, JSON形式をhash形式に変換する (JSONの結果をHashに変換する) #symbolize_names: trueでシンボルのキーをシンボルのままでhashにする。 result = JSON.parse(json, {symbolize_names: true}) #④digメソッドでhashから値を取り出す(Hashから値を取り出す) puts result.dig(:results, 0,:address1) これを実行すると, 神奈川県という結果が返ってきます 参考記事 ・MDN Web Docs | Web APIの紹介 ・Ruby 3.0.0 リファレンスマニュアル library net/http ・Ruby 3.0.0 リファレンスマニュアル Hashクラス ・Ruby 3.0.0 リファレンスマニュアル JSON.#parce ・WebAPIについての説明 ・郵便番号API ・RubyでJSON形式の結果が帰ってくるURLをParceする ・RubyでネストしたHashやArrayから値を取り出す方法いろいろ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyでJson形式の結果を返すWebAPIを使ってみよう(ハンズオン)

はじめに この記事は, rubyでweb apiの使いかたを理解するための初心者向けの記事です。具体的には, rubyで郵便APIを使用し, コンソールに表示するまでを行います。 バージョン : ruby3.0.0 WebAPIとは何か WebAPIの概説 みなさん、WebAPIって何かわかりますか!? シーン.... おっと(笑)。でも大丈夫ですよ、丁寧に説明していこうと思います。厳密な定義などが知りたい方は, 記事に記載されている公式リファレンスを参照してくださいね。 まず、APIについて見ていきましょう。Mozzilaの公式リファレンスの説明によるとAPIは次のように説明されています。 Application Programming Interfaces (APIs) は、開発者が複雑な機能をより簡単に作成できるよう、プログラミング言語から提供される構造です。 平たく言うと, 複雑なコードで書かれた難しいプログラムを, 誰でも簡単に扱えるようにしたものです。 そして, このAPIのうちサーバからデータ取得をする APIのことをWebAPIと呼びます。 例えば、WebAPIでは,TwitterAPIがありますが、これは最新のツイートをwebサイトに表示するという機能を簡単に扱えるようにしてあるAPIです。他にも,GoogleAPIやFacebook API、YouTube APIなど様々なAPIが提供されています。 WebAPIを使ってみよう 流れ それでは、実際にWebAPIを使っていきましょう。 流れとしては、 ①uriモジュールを使って, URIをパースする (URIの解析) ②net/httpモジュールを使って, APIにget通信でAPIのサーバにアクセスして、サーバからJSON形式の結果をもらう。(HTTP通信でAPIにアクセス) ③jsonモジュールを使って, JSON形式をhash形式に変換する (JSONの結果をHashに変換する) ④digメソッドでhashから値を取り出す(Hashから値を取り出す) 郵便APIの構成 ここで試しに使う郵便APIは,以下のとおりです。?以降のzipcode=〇〇に郵便番号を指定する事で住所を返してくれます。ブラウザでこのコードにアクセスすると, APIにアクセスした結果が表示されます。 http://zipcloud.ibsnet.co.jp/api/search?zipcode=2500011 実装(ハンズオン) 最後にコードで実装すると次のようになります require 'net/http' require 'uri' require 'json' #①uriモジュールを使って, URIをパースする (URIの解析) uri = URI.parse('http://zipcloud.ibsnet.co.jp/api/search?zipcode=2500011') #②net/httpモジュールを使って, APIにget通信でAPIのサーバにアクセスして、サーバからJSON形式の結果をもらう。(HTTP通信でAPIにアクセス) json = Net::HTTP.get(uri) #③jsonモジュールを使って, JSON形式をhash形式に変換する (JSONの結果をHashに変換する) #symbolize_names: trueでシンボルのキーをシンボルのままでhashにする。 result = JSON.parse(json, {symbolize_names: true}) #④digメソッドでhashから値を取り出す(Hashから値を取り出す) puts result.dig(:results, 0,:address1) これを実行すると, 神奈川県という結果が返ってきます 参考記事 ・MDN Web Docs | Web APIの紹介 ・Ruby 3.0.0 リファレンスマニュアル library net/http ・Ruby 3.0.0 リファレンスマニュアル Hashクラス ・Ruby 3.0.0 リファレンスマニュアル JSON.#parce ・WebAPIについての説明 ・郵便番号API ・RubyでJSON形式の結果が帰ってくるURLをParceする ・RubyでネストしたHashやArrayから値を取り出す方法いろいろ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ruby 正規表現でサブドメインを許可する

備忘録です regexp = /\Ahttps:\/\/(.*\.)?hogehoge.com\z/ url = 'admin.hogehoge.com' url.match?(regexp)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails モデル作成時に関連付け(1対多の場合)

※ 関連付け(アソシエーション)とは テーブル間の関係を、モデル上の関係として操作できるようにする事。 【モデル作成時に関連付けを設定する方法】 例として、1人のユーザー(Userモデル)が多くのつぶやき(Tweetモデル)を作成する場合。 ※Userモデルはすでに存在しているものとします。 1. Modelの作成 $ rails g model Tweet content:string user:references user:referencesの指定で、Tweetモデルにuser_idカラムが作成されます。 ※references=参照 2. 作成されたマイグレーションファイルの確認 20210414_create_tweets.rb class CreateTweets < ActiveRecord::Migration[5.2] def change create_table :tweets do |t| t.string :content t.references :user, foreign_key: true # foreign_key: true ? 外部キー制約 t.timestamps end end end ※外部キー制約:  1. Usersテーブルに存在しない値を外部キーとして登録不可  2. Tweetsテーブルの外部キーに値が登録されているUsersテーブルのレコードは削除不可 3. マイグレーション実行 rails db:migrate 4. shema.rbで確認 app/db/schema.rb create_table "tweets", tweets: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.string "content" t.bigint "user_id" # reference型でuser_idカラム生成 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["user_id"], name: "index_opinions_on_user_id" end 5. Tweetモデルを確認・Userモデルを編集 app/models/tweet.rb class Tweet < ApplicationRecord belongs_to :user end 作成されたtweet.rbにはuserに属する(belongs_to)が記述されます。 これに対して、Userモデルにtweetを所有する(has_many)を追記します。 app/models/user.rb class User < ApplicationRecord has_many :tweets, dependent: :destroy # 多数のつぶやきを持つ為、tweetsと複数形にする end ※dependent: :destroyは、先に書いた外部キー制約でエラーが出ない為の設定。  これにより、Userが削除された場合、属するtweetsも削除されます。 以上の設定で、User has many Tweets、Tweet belongs to userの関連付けができます。 関連付けしていない・している場合の例 ユーザーのつぶやきを取得 ・関連付け無し場合 @user = User.find(params[:id]) Tweets = Tweet.where(user_id: @user.id) ・関連付けした場合 @user = User.find(params[:id]) Tweets = @user.tweets 省略できますね!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

translation missing: ja~ 系のエラー。日本語、英語どちらも対応させる。

前提知識 なぜこのエラーが出たのか ja.yml内に、該当する値が存在しないから 以前Fakerを日本語化したため、Railsアプリのデフォルトが日本語になっていた。 application.rb config.i18n.available_locales = %i[ja en] config.i18n.default_locale = :ja ↓ Rspecを行った。 spec/models./post_spec.rb it 'is invalid without a title' do post = Post.new() post.valid? expect(post.errors.messages[:title]).to include('can`t be blank') end ↓ エラー発生 Failure/Error: expect(post.errors.messages[:title]).to include('Can not be blank') expected #<ActiveModel::DeprecationHandlingMessageArray(["translation missing: ja.activerecord.errors.models.post.attributes.title.blank"])> to include "Can not be blank" 大事なところ translation missing: ja.activerecord.errors.models.post.attributes.title.blank" ↓ ja.ymlのja.activerecord.errors.models.post.attributes.title.blankに対する値がないというエラー。 解決方法 ja.ymlにエラーに書かれている内容通りに設定する "ja.activerecord.errors.models.post.attributes.title.blank" ja.yml ja: activerecord: errors: models: post: attributes: title: blank: "タイトルが空白です" rails cで確認してみる pry(main)> I18n.t("activerecord.errors.models.post.attributes.title.blank") => "タイトルが空白です。" rspecを直す spec/models./post_spec.rb it 'is invalid without a title' do post = Post.new() post.valid? - expect(post.errors.messages[:title]).to include('can`t be blank') + expect(post.errors.messages[:title]).to include(I18n.t("activerecord.errors.models.post.attributes.title.blank")) end Rspec無事通りました。 参考: https://railsguides.jp/i18n.html#%E8%A8%B3%E6%96%87%E3%81%AE%E5%8F%82%E7%85%A7 https://qiita.com/punkshiraishi/items/bdb2d48425782e25eadc https://qiita.com/shimadama/items/7e5c3d75c9a9f51abdd5 https://pikawaka.com/rails/i18n
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでCSSを遅延読み込みさせる

stylesheet_link_tag に preload を付与する。 PageSpeedInsight のスコアアップのために rails で CSS をプリロードさせてファーストビューのレンダリング速度をあげたい。 stylesheet_link_tag に media onload rel preload 属性を付与してあげる。 変更前.erb <%= stylesheet_link_tag 'app_basic', media: 'all' %> 変更後.erb <%= stylesheet_link_tag 'app_basic', media: 'all', onload: "this.onload=null;this.rel='stylesheet'", rel: 'preload', as: 'style' %> <noscript><%= stylesheet_link_tag 'app_basic' %></noscript> preload に対応していないブラウザがあるため、noscriptタグで通常のCSS読み込み処理を書いてあげれば完成。 Chromeの場合は設定から、javascriptを許可しないモードにすればデバッグできます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでCSSをpreloadさせる

stylesheet_link_tag に preload を付与する。 PageSpeedInsight のスコアアップのために rails で CSS をプリロードさせてファーストビューのレンダリング速度をあげたい。 stylesheet_link_tag に media onload rel preload 属性を付与してあげる。 変更前.erb <%= stylesheet_link_tag 'app_basic', media: 'all' %> 変更後.erb <%= stylesheet_link_tag 'app_basic', media: 'all', onload: "this.onload=null;this.rel='stylesheet'", rel: 'preload', as: 'style' %> <noscript><%= stylesheet_link_tag 'app_basic' %></noscript> preload に対応していないブラウザがあるため、noscriptタグで通常のCSS読み込み処理を書いてあげれば完成。 Chromeの場合は設定から、javascriptを許可しないモードにすればデバッグできます。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTMLへの埋め込み用としてRubyを使う(勉強メモ)

※勉強メモです。 <<-PAGE ... PAGE:ヒアドキュメント形式 改行が重なるような文字列の記述に向く この間に記述したものを、ターミナルでrubyとして実行すると、単に文字列のHTMLが表示される(HTMLをRubyで実行して表示) ・ERBを使ってRubyコードをHTMLに埋め込む <%.. Rubyコード..%>や<%=..Rubyコード..%>をHTMLに埋め込む 「result」メソッドで埋め込まれたコードを実行し、最終的にHTMLに出力する機能 require 'erb' #ERBライブラリの読み込み def web_page ... <p><%= Time.now %></p> ... end erb = ERB.new(web_page) #ERBオブジェクトのインスタンス生成 result = erb.result #ERB内のRubyコード実行 puts result #文字列表示 ・Webサーバーと起動してRubyを自動的に実行させる 「Webサーバー-Pumaとは」 Webサーバー:HTTPリクエストを受取り、HTMLなどのWebページをHTTPレスポンスとして返すサーバのこと Puma:RubyやRails標準のWebサーバー ・Gem Rubyのライブラリ(便利機能をパッケージングした拡張機能) ・Webサーバーの起動 Sinatra:軽量なWebアプリケーションフレームワーク Sinatra では、views フォルダの中には erb ファイルのみを配置し、views/ フォルダの中の index.erb が今までの HTML に埋め込んできた ERB となり、このように独立のファイルとして作成 app.rb には、HTTPリクエストが来たときのWEBアプリの処理を記述 require 'sinatra' #Sinatraの読込み get '/' do #トップページ(/)にHTTPリクエストのGETメソッドが来た時に対処する処理(do ~ end) erb :index #views/index.erb内の埋込Rubyの実行結果として出力されたHTMLをレスポンスとして返す end ・Webサーバーの起動 sinatra-webフォルダに移動$ cd ~/enviromnet/sinatra-ewb Pumaの起動$ ruby app.rb -o $IP -p $PORT 「フォームデータの送受信」 ・GETとPOST ブラウザからHTTPリクエストを送る際の技術的な仕様 ・GET:基本的にほかユーザーにも表示される情報  リンクをクリックしたとき  ブラウザのURL欄に直接URLを打込んだとき  ブラウザで検索ボタンを押したとき ・POST:ほかユーザーに見られたくない情報の送信  問合せフォームで内容を入力し、送信ボタンをクリックしたとき  IDとパスワードを入力し、ログインボタンをクリックしたとき ・POSTでフォームから情報を送信する (例)名前を入力し送信 (view/index.erb) #送信先の指定:/(トップ) POSTメソッド指定:method="POST" <form action="/" method="POST"> <label>名前: <input type="text" name="target_name"></label> <input type="submit" value="送信"> </form> ・POSTで送信した情報を受け取って処理する target_nameを処理する (app.rb)に以下追加 post '/' do erb :index end ※app.rb を変更する度、Puma を再起動(Ctrl+c) ・index.erbでPOSTデータ受取り (view/index.erb) <% name = params['target_name'] %> ★HTTPリクエストのPOSTメソッドで送ったデータはRubyのparams(Parameters・パラメータの略)に格納される POSTメソッドで、ユーザーからHTMLのform要素(入力フォーム)を通してデータを受け取れる 受取ったデータはparamsという変数に自動的に格納される(paramsはハッシュ)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

privateなgemがCould not find Xxxで見つからない時

これ何 sidekiq-proの最新がbundle installでlocalに入ってこなかった。 その時に対応したことメモ。 エラーメッセージ $ bundle install [DEPRECATED] Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. You should run `bundle update` or generate your lockfile from scratch. Fetching gem metadata from https://gems.contribsys.com/.. Fetching gem metadata from https://rubygems.org/......... Fetching gem metadata from https://gems.contribsys.com/.. Could not find sidekiq-pro-5.2.2 in any of the sources 解決方法 ~/.bundle に入っているキャッシュを削除する $ rm ~/.bundle
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]継続的にプロダクト開発していくために必要な設定たち

Railsを使えば簡単にWebアプリケーションを立ち上げることができます。 例えば、Railsガイドに書かれている手順通りに実施するだけで1時間もかからずに画面を表示することができると思います。 静的なページだけではなく、scaffoldを使うことで簡単なデータ操作を一通りできる画面もサクッと作ることができます。 以前私が書いた『Rails newからproductionモードで動くようになるまで』では、1歩踏み込んでproductionモードで動くまでの手順を書きました。 ただ、上記はproductionモードで動いているものの、プロダクト開発で使うためには他にも様々な設定が必要です。 プロダクトで使えるようにするためには、同期的な処理だけでは足りず、ほとんどの場合に非同期処理を行う仕組み(Active job)やメール配信の仕組み(Action Mailer)などが必要になります。 また、外部接続情報など環境ごとに設定値を持つようにしたり、継続的に開発できるようにCIを設定したり、誰でも同じ環境で開発できるようにどのマシンでも開発環境を再現できるようにしたり、様々なことをする必要があります。 この記事ではrails newで作ったRailsアプリケーションに上記に書いたプロダクト開発するために必要になるであろう設定を入れる手順をまとめました。 ※私個人が最近はRailsをAPIアプリケーションとしか扱っていないため、この記事ではAPIモードで構築します。 前提 開発環境: Docker ソース管理: GitHub CI: GitHub Actions 各種バージョンは下記の通り Ruby: 3.0 Rails: 6.1.1→6.1.3.1 APIモード 記事を書いている途中でライセンス問題が発生して動かなくなったのでバージョンアップ MySQL: 8.0.23 Redis: 6.0.9 Githubにリポジトリを作る 今の時代、ソース管理は必須ですよね。GitHubに新しいリポジトリを追加します。 READMEや.gitignoreは rails newした時に生成されるので不要です。 リポジトリができたらローカルにCloneしましょう。 % git clone git@github.com:ham0215/rails_api_base.git Cloning into 'rails_api_base'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), done. % cd rails_api_base rails_api_base % ls LICENSE Dockerの準備 開発環境はどのマシンでも同じ環境を再現しやすくするためにDockerで構築します。 Dockerfile 書き方は様々あると思いますが、下記のような感じにしています。 localeやvimは必須ではないのですが、コンテナ上でvimを使ったり日本語入力できるようにするために入れています。 Dockerfile FROM ruby:3.0.0 RUN apt-get update && apt-get install -y \ build-essential \ vim \ locales \ locales-all \ default-mysql-client \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* ENV LANG ja_JP.UTF-8 RUN mkdir /app WORKDIR /app COPY Gemfile Gemfile COPY Gemfile.lock Gemfile.lock RUN bundle install COPY . . CMD ["rails", "server", "-b", "0.0.0.0"] Dockerfile内でGemfileとGemfile.lockは明記しており、ファイルがないとエラーになってしまうのでファイルを作っておきます。 Gemfileはrailsだけ記述しておき、Gemfile.lockは空でOKです。 Gemfile source 'https://rubygems.org' gem 'rails', '6.1.1' ここまでできたら一度ビルドしてみます。 DOCKER_BUILDKITを指定すると少し高速化され、コンソールの表示が見やすくなるのでオススメです。 % COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose build db uses an image, skipping WARNING: Native build is an experimental feature and could change at any time Building api ... Successfully built c20632ba2529ddbf7702f9df80ded5d28955d0272290c57ebbdb01b65f55b5ed docker-compose.yml MySQLもローカルから接続できるようにportsを指定しています。 portはデフォルトのままだと他のアプリケーションと被ることが多いので少しずらすと良いです。 docker-compose.yml version: '3.8' services: db: image: mysql:8.0.23 environment: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" ports: - '3308:3306' volumes: - ./mysqlcnf.d:/etc/mysql/conf.d - ./tmp/mysql:/var/lib/mysql api: tty: true stdin_open: true build: . command: rails s -b 0.0.0.0 volumes: - .:/app environment: DB_HOST: db ports: - "3001:3000" depends_on: - db dbのvolumesに指定している- ./mysqlconf.d:/etc/mysql/conf.dはMySQLの認証方法を変更するために指定しています。 mysqlcnf.d/custom.cnf [mysqld] default_authentication_plugin=mysql_native_password 詳細は下記の記事をご覧ください。 下記の記事はGitHub Actionsの話ですが、同様のことを行っています。 rails new 次にRailsアプリケーションを作成します。 APIモードで作成するので--apiをつけています。その他不要なものはskipしています。 docker-compose run api bundle exec rails new . --database=mysql --skip-action-mailbox --skip-action-text --skip-spring --skip-turbolinks --skip-bootsnap --skip-action-cable --skip-javascript --skip-jbuilder --skip-system-test --api --skip-test --force rails newが正常終了して必要なファイルが生成されたら、必要最低限の設定を行ってサーバーを立ち上げてみます。 まずDB接続情報の修正が必要です。 docker-composeでつけた名前がhostとして使えるので修正します。 config/database.yml - host: localhost + host: db 接続情報を直したらDBを作成して、空のschema.rbを作っておきましょう。 % docker-compose run api rails db:create % docker-compose run api rails db:migrate 一通り設定ができたのでDockerを立ち上げてサーバーにアクセスしてみます。 下記コマンドでコンテナをバックグラウンドで立ち上げます。 "COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose up --build -d" 立ち上がったらブラウザで画面を表示してみましょう。 http://localhost:3001/ お馴染みの下記画面が出たらここまでの手順は成功です! database setting 最初にデータベースのcharacter_setを確認しておきます。 データベースの設定は途中で変える場合、面倒なことになることが多いので必ず最初に確認しましょう。 MySQLに接続します。 db:createとdb:migrateを行っているのでDBとテーブルが作成されています。 % mysql -h 127.0.0.1 -P3308 -uroot mysql> show databases; +--------------------+ | Database | +--------------------+ | app_development | | app_test | | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 6 rows in set (0.01 sec) mysql> use app_development Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed mysql> show tables; +---------------------------+ | Tables_in_app_development | +---------------------------+ | ar_internal_metadata | | schema_migrations | +---------------------------+ 2 rows in set (0.01 sec) character_set character_setを確認します。 mysql> show variables like '%char%'; +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ 8 rows in set (0.01 sec) database.ymlに下記設定があるため、ほとんどの項目はutf8mb4が設定されています。 config/database.yml encoding: utf8mb4 特にこだわりがなければutf8mb4を使えばよいと思います。 character_set_systemだけutf8ですが、こちらはutf8mb4は設定できないので問題ありません。 utf8mb4はutf8を拡張したものです。詳細は下記をご覧ください。 照合順序 続いて照合順序を確認します。 mysql> SELECT @@collation_database; +----------------------+ | @@collation_database | +----------------------+ | utf8mb4_0900_ai_ci | +----------------------+ 1 row in set (0.00 sec) MySQL8系からデフォルトの照合順序がutf8mb4_0900_ai_ciになったようです。 下記のブログがとても分かりやすかったので参考にさせていただきました。 日本語を使う環境であれば、utf8mb4_general_ciかutf8mb4_binを選ぶのが良いと思います。 この表だけで考えると英語の大文字/小文字が区別できるutf8mb4_binが良さそうに見えますが、文字列型でよくユニーク制約を付けるメールアドレスの比較は大文字/小文字を区別しない方が嬉しいのでutf8mb4_general_ciが便利だったりします。 今回はcollation: utf8mb4_general_ciに変更します。 database.ymlにcollation: utf8mb4_general_ciを設定してDBを作り直します。 作り直すときはvolumsで指定しているtmp/mysql/配下を消し忘れないようにご注意ください。 Dockerを起動し直して、改めてdb:createを行い変更されていることを確認します。 mysql> use app_development Database changed mysql> SELECT @@collation_database; +----------------------+ | @@collation_database | +----------------------+ | utf8mb4_general_ci | +----------------------+ 1 row in set (0.00 sec) ログをjsonにする Railsではデフォルトで下記のようなログが出力されます。 log/development.log Started GET "/" for 172.24.0.1 at 2021-02-24 14:49:00 +0000 (0.7ms) SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC Processing by Rails::WelcomeController#index as HTML Rendering /usr/local/bundle/gems/railties-6.1.1/lib/rails/templates/rails/welcome/index.html.erb Rendered /usr/local/bundle/gems/railties-6.1.1/lib/rails/templates/rails/welcome/index.html.erb (Duration: 11.5ms | Allocations: 406) Completed 200 OK in 52ms (Views: 33.8ms | ActiveRecord: 0.0ms | Allocations: 2208) 開発中は上記のログで特に問題ないのですが、本番のログなどは監視ツール等に取り込みたいなどありjson形式にしたいことがあります。 そんなときに役立つgemがlogrageです。 Gemfileに追加して最低限の設定を入れてみました。 元のログも残したいので別ファイルに出力するようにしています。 設定方法の詳細はREADMEをご覧ください。 config/initializers/lograge.rb Rails.application.configure do return if Rails.env.test? config.lograge.enabled = true config.lograge.keep_original_rails_log = true config.lograge.logger = ActiveSupport::Logger.new "#{Rails.root}/log/lograge_#{Rails.env}.log" config.lograge.formatter = Lograge::Formatters::Json.new end 下記のようにjson形式のログも出力されるようになりました。 log/lograge_development.log {"method":"GET","path":"/","format":"html","controller":"Rails::WelcomeController","action":"index","status":200,"duration":44.07,"view":28.32,"db":0.0} RSpec, factory-bot, faker, simplecov 継続的に開発するアプリケーションにはテストコードが必須です。 テストを行うgemは様々あると思いますが、メジャーなRSpecを使うと良いと思います。 RSpec関連のgemを入れます。私がよく使うgemは下記の通り。 rspec-rails RailsでRSpecを使えるようにするgem factory_bot_rails テストデータを生成するためのgem faker ダミーデータ(適当な名前やメールアドレスなど)をいい感じに生成してくれるgem simplecov テストカバレッジを出力してくれるgem simplecov-json simplecovのテストカバレッジをjsonにしてくれるgem では早速インストールします。 各gemのREADMEに記載されている通り、Gemfileに追加してinstallするだけです。 本番環境では必要ないのでdevelopmentやtestのgroupに追加しましょう。 ここからは各Gemについてもう少し詳細に書いていきます。 rspec-rails インストールするためのコマンドがあるので実行します。 % docker-compose exec api rails g rspec:install create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rb RSpecで必要なファイルが生成されます。 Ruby2.7.2からdeprecated warningがデフォルトで出力されなくなったのですが、test時には出力されたほうが嬉しいので出力されるように下記の記述を追加しました。 spec/spec_helper.rb Warning[:deprecated] = true factory_bot_rails 設定不要で使えますが、下記の記述を書いておくとFactoryBot.createなどのFactoryBotを省略してcreateだけで使えるようになるので便利です。 spec/rails_helper.rb config.include FactoryBot::Syntax::Methods faker 特に初期設定は不要です。 simplecov, simplecov-json READMEを参考に設定を行います。 設定はconfig/initializers配下に置きました。 出力は人が見やすいHTMLとプログラムで扱いやすいJsonの2種類にしています。 config/initializers/simplecov.rb return unless Rails.env.test? require 'simplecov' require 'simplecov-json' SimpleCov.formatters = [ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::JSONFormatter, ] .simplecovにadd_filterでカバレッジを取得する必要がないフォルダーを指定します。 設定ファイルやマイグレーションファイル、テストファイルを除いています。 .simplecov SimpleCov.start('rails') do add_filter 'config' add_filter 'spec' add_filter 'db' end RSpec実行 設定したのでテストを1つ書いてみます。 と言っても、まだ1つもアクションがないのでテストと一緒に作成してみます。 簡単にscaffoldを使ってUserリソース(カラムはnameのみ)を作ってみます。 RSpecやFactoryBotを導入していた後なので、controller / model / migration だけではなく、RSpecやFactoryBotのファイルも自動生成されました。 % docker-compose exec api rails g scaffold User name:string invoke active_record create db/migrate/20210311150016_create_users.rb create app/models/user.rb invoke rspec create spec/models/user_spec.rb invoke factory_bot create spec/factories/users.rb invoke resource_route route resources :users invoke scaffold_controller create app/controllers/users_controller.rb invoke resource_route invoke rspec create spec/requests/users_spec.rb create spec/routing/users_routing_spec.rb 簡単に動作させるなら何も編集しなくても良いレベルのソースが書かれているのでそのまま利用します。 まずはマイグレーションを実行してテストDBにも反映します。 % docker-compose exec api rails db:migrate == 20210311150016 CreateUsers: migrating ====================================== -- create_table(:users) -> 0.0302s == 20210311150016 CreateUsers: migrated (0.0303s) ============================= % docker-compose exec api rails db:test:prepare % docker-compose exec api rspec ***********...... Pending: (Failures listed here are expected and do not affect your suite's status) 1) User add some examples to (or delete) /app/spec/models/user_spec.rb # Not yet implemented # ./spec/models/user_spec.rb:4 2) /users GET /index renders a successful response # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:36 3) /users GET /show renders a successful response # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:44 4) /users POST /create with valid parameters creates a new User # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:53 5) /users POST /create with valid parameters renders a JSON response with the new user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:60 6) /users POST /create with invalid parameters does not create a new User # Add a hash of attributes invalid for your model # ./spec/requests/users_spec.rb:69 7) /users POST /create with invalid parameters renders a JSON response with errors for the new user # Add a hash of attributes invalid for your model # ./spec/requests/users_spec.rb:76 8) /users PATCH /update with valid parameters updates the requested user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:91 9) /users PATCH /update with valid parameters renders a JSON response with the user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:99 10) /users PATCH /update with invalid parameters renders a JSON response with errors for the user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:109 11) /users DELETE /destroy destroys the requested user # Add a hash of attributes valid for your model # ./spec/requests/users_spec.rb:120 Finished in 0.13536 seconds (files took 5.92 seconds to load) 17 examples, 0 failures, 11 pending Coverage report generated for RSpec to /app/coverage. 14 / 29 LOC (48.28%) covered. Coverage report generated for RSpec to /app/coverage/coverage.json. 14 / 29 LOC (48.28%) covered. user_spec.rbのテストはすべてpendingになっているので実行できるように修正します。 テストを直す前にUserモデルのバリデーション失敗をテストするために、nameに必須制約を入れておきます。 app/models/user.rb class User < ApplicationRecord validates :name, presence: true end users_specはskipのところを修正しました。 nameの生成にはFakerを使っています。 spec/requests/users_spec.rb let(:valid_attributes) { - skip("Add a hash of attributes valid for your model") + { name: Faker::Name.name } } let(:invalid_attributes) { - skip("Add a hash of attributes invalid for your model") + { name: '' } } # This should return the minimal set of values that should be in the headers @@ -85,7 +85,7 @@ RSpec.describe "/users", type: :request do describe "PATCH /update" do context "with valid parameters" do let(:new_attributes) { - skip("Add a hash of attributes valid for your model") + { name: Faker::Name.name } } it "updates the requested user" do @@ -93,7 +93,7 @@ RSpec.describe "/users", type: :request do patch user_url(user), params: { user: new_attributes }, headers: valid_headers, as: :json user.reload - skip("Add assertions for updated state") + expect(user.name).to eq new_attributes[:name] end 再実行してみましたが、headerのチェックでこけてしましました。 % docker-compose exec api rspec *.....F..F....... Failures: 1) /users POST /create with invalid parameters renders a JSON response with errors for the new user Failure/Error: expect(response.content_type).to eq("application/json") expected: "application/json" got: "application/json; charset=utf-8" (compared using ==) # ./spec/requests/users_spec.rb:80:in `block (4 levels) in <top (required)>' 2) /users PATCH /update with invalid parameters renders a JSON response with errors for the user Failure/Error: expect(response.content_type).to eq("application/json") expected: "application/json" got: "application/json; charset=utf-8" (compared using ==) # ./spec/requests/users_spec.rb:114:in `block (4 levels) in <top (required)>' Finished in 1.52 seconds (files took 5.43 seconds to load) 17 examples, 2 failures, 1 pending 調べてみるとRials6.1でresponse.content_typeで返却される値が変わったようです。 ということなので、エラーになっているテストのexpected valeueを変更します。 よく見たらcontent_typeの部分一致で比較している箇所もあったので合わせてみました。これはRSpecのテンプレートの修正漏れなのかな? → rspec-railsに該当箇所のプルリクを送ったらマージされたので次のバージョンからはこの事象は発生しなくなると思います。 spec/requests/users_spec.rb - expect(response.content_type).to eq("application/json") + expect(response.content_type).to match(a_string_including("application/json")) 再実行したら全て成功しました。 モデルスペックがpendingのままですが今回は省略します。直す気がないpendingは残しておいても邪魔なので削除しておくと良いです。 % docker-compose exec api rspec *................ Pending: (Failures listed here are expected and do not affect your suite's status) 1) User add some examples to (or delete) /app/spec/models/user_spec.rb # Not yet implemented # ./spec/models/user_spec.rb:4 Finished in 1.44 seconds (files took 6.11 seconds to load) 17 examples, 0 failures, 1 pending Coverage report generated for RSpec to /app/coverage. 28 / 30 LOC (93.33%) covered. Coverage report generated for RSpec to /app/coverage/coverage.json. 28 / 30 LOC (93.33%) covered. 最後にRSpec実行時の最後に記載されているカバレッジをみてみましょう。 coverage/index.htmlをブラウザで開いてみると下記のようにファイルごとのカバレッジが見れます。 今回追加したuser系のファイルは100%になっているのでテストは網羅されてそうです! coverage配下にjson形式のファイルも入っています。 simplecovが出力するカバレッジファイルはコミット不要なので.gitignoreにcoverageディレクトリを追加しておきましょう。 .gitignore + coverage RuboCop RuboCopはコードを静的解析してくれるGemです。 これを入れておくことでコードの記述揺れやインデントやスペースの入れ方などをルールに基づいて機械的にチェックすることができます。 例えば下記のようなチェックを行ってくれます。 # `{`の後のスペースがいらない app/models/user.rb:13:19: C: [Correctable] Layout/SpaceInsideBlockBraces: Space between { and | detected. (1..10).each { |n| p n } ^ # hogeが定義されているけど使われていない app/models/user.rb:25:5: W: Lint/UselessAssignment: Useless assignment to variable - hoga. hoge = 'hoge' ^^^^ # 最終行はreturnを書かなくても良い app/models/user.rb:26:5: C: [Correctable] Style/RedundantReturn: Redundant return detected. return true ^^^^^^ 上記のような機械的にできるチェックをレビューで人がやるのは時間の無駄になるだけでなく、体裁ばかりに目がいってしまい本来に抽出したい複雑な不具合などに目がいかなくなってしまいます。 このような事象を回避するために静的コードチェックツールは必須と言えるでしょう。 READMEに記載されている通り、Gemfileに追加してinstallしましょう。 本番環境では必要ないものなのでdevelopmentやtestのgroupに追加しましょう。 インストールできたら早速実行してみます。 % docker-compose exec api rubocop Inspecting 38 files CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Offenses: .simplecov:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment. SimpleCov.start('rails') do ...(略) 38 files inspected, 196 offenses detected, 183 offenses auto-correctable 196個も指摘されました・・・ 様々な原因がありますが、現時点ではまだほとんど実装していないので指摘されている箇所はRailsが自動生成したファイルが多いです。 私はざっくり下記の方針で直しています。 Railsが自動生成したファイル 今後触るファイル: RuboCopのルールに合わせて修正 今後ほぼ触らないファイル: RuboCopの対象外にする migrationファイルやconfigは対象外 まずはこの方針に合わせて設定ファイル(.rubocop.yml)を設定しました。 NewCops: enableを書いておくことで、バージョンアップで追加されるチェック項目が自動的に有効になります。 .rubocop.yml AllCops: Exclude: - 'bin/**/*' - 'config/**/*' - 'config.ru' - 'db/**/*' - 'Gemfile' - 'spec/**/*' - 'vendor/**/*' NewCops: enable この設定を入れて再実行すると21個まで減りました。 8 files inspected, 21 offenses detected, 19 offenses auto-correctable 残りは[修正する / チェックを無効(またはゆるく)にする]を考えながら見ていきます。 最終的に下記のチェックだけ無効にして他はコードを修正しました。 Style/Documentationを無効 classの先頭にドキュメントがないとNGになる。コメントは必要な時だけ追加すれば良いと思っているので無効化。 まだコード量が少ないのでほとんど引っ掛かりませんでしたが、今後コードを追加していくと新しくNGになることがあると思います。 特にMetrics/AbcSizeやMetrics/MethodLengthは曲者です。 無理やりメソッドを分割することでNGを回避することができるのですが、可読性を考えて分割しない方がよいという判断もありえます。 RuboCopのチェックを全て正とするのではなく[修正する / チェックを無効(またはゆるく)にする]を吟味して開発速度が最大化される方向にチェック仕様をアップデートしていきましょう。 Brakeman Brakemanはコードを静的解析して脆弱性を検知してくれるgemです。 これを入れておくことで典型的な脆弱性となり得るコードを機械的に検知することができます。 あくまで典型的なものを防げるだけですが、入れておいて損はないでしょう。 READMEに記載されている通り、Gemfileに追加してinstallするだけです。 本番環境では必要ないのでdevelopmentやtestのgroupに追加しましょう。 早速実行します。 オプションはこのページに細かく記載されています。 # Rails6系なので -6 を指定 # 全てのチェック項目を実施したいので -A を指定 # 全ての警告を検知したいので -w 1 を指定 % docker-compose exec api brakeman -6 -A -w 1 ... == Overview == Controllers: 2 Models: 2 Templates: 0 Errors: 0 Security Warnings: 1 == Warning Types == Missing Encryption: 1 == Warnings == Confidence: High Category: Missing Encryption Check: ForceSSL Message: The application does not force use of HTTPS: `config.force_ssl` is not enabled File: config/environments/production.rb Line: 1 1つ警告が検知されました。 productionではconfig.force_sslを有効にしろとのことです。 こちら本番環境の構成によって設定すべきかどうか違うと思いますが、今回はとりあえずtrueにしておきます。 対応不要な場合はbrakeman.ignoreというファイルを生成することでチェック対象外にできます。 config/environments/production.rb - # config.force_ssl = true + config.force_ssl = true これで警告はなくなりました。 % docker-compose exec api brakeman -6 -A -w 1 ... == Overview == Controllers: 2 Models: 2 Templates: 0 Errors: 0 Security Warnings: 0 == Warning Types == No warnings found tbls tblsはデータベースのスキーマ情報からテーブル定義のドキュメントを生成してくれるツールです。 テーブル定義に限りませんが、手動でメンテしているドキュメントは本番環境と乖離してしまうので、tblsのようにコードからリバースエンジニアリングできるツールは重要です。 こちらはDockerが提供されているのでそれを使います。 docker-composeに追記しました。 設定値の詳細はREADMEをご覧ください。 下記のように設定することでdocs/tables配下にテーブル定義が出力されます。 docker-compose.yml + tbls: + image: k1low/tbls:latest + volumes: + - .:/work + environment: + TBLS_DSN: mysql://root:@db:3306/app_development + TBLS_DOC_PATH: docs/tables + depends_on: + - db 早速実行してみます。 --forceをつけることでファイルがあっても上書きするようにしています。 % docker-compose run --rm tbls doc --force Creating rails_api_base_tbls_run ... done docs/tables/schema.svg docs/tables/ar_internal_metadata.svg docs/tables/schema_migrations.svg docs/tables/users.svg docs/tables/README.md docs/tables/ar_internal_metadata.md docs/tables/schema_migrations.md docs/tables/users.md 下記のMarkdownが生成されました。 シードデータ Dockerで環境構築していると環境を簡単にリセットすることができますが、そのたびにデータまで初期化されてしまっては面倒です。 そこで開発時にあったら便利なデータはシードデータを作っておき、いつでもロードできるようにしておくと良いです。 また、マスターデータなどを入れる際にもシードデータを作っておくと便利です。 シードデータの作成はseed-fuというgemが便利です。 Gemfileに追加してインストールしましょう。 2018年から更新されていないのでドキュメントなどが古いですが、Rails6でも問題なく使えます。 Gemfile +gem 'seed-fu' development環境用のシードデータを作ってみます。 db/fixtures/development/users.rb User.seed(:id, { id: 1, name: 'hoge' }, { id: 2, name: 'fuga' }, ) 実行します。 % docker-compose exec api rails db:seed_fu == Seed from /app/db/fixtures/development/users.rb - User {:id=>1, :name=>"hoge"} - User {:id=>2, :name=>"fuga"} データベースに登録されました。 mysql> select * from users; +----+------+----------------------------+----------------------------+ | id | name | created_at | updated_at | +----+------+----------------------------+----------------------------+ | 1 | hoge | 2021-03-15 15:03:33.725453 | 2021-03-15 15:03:33.725453 | | 2 | fuga | 2021-03-15 15:03:33.734686 | 2021-03-15 15:03:33.734686 | +----+------+----------------------------+----------------------------+ 2 rows in set (0.00 sec) seed-fuのいいところは指定したキーが重複している場合はUpdateになるところです。 上記の場合はidをキーとしてデータが存在している時はUpdateとして動作します。 そのため重複実行してもエラーになったり、実行ごとにデータが増加していったりすることはありません。 Security Alert / dependabot Gemfileでインストールしているgemのバージョンを定期的にチェックし、脆弱性があるバージョンを使っていたり、新しいバージョンがある場合に通知してくれるサービスがあります。 継続的に開発していく場合、ライブラリのアップデートは必要不可欠なので検知できるように設定しておきましょう。 Security Alert GitHubのリポジトリページのSettingsタブのSecurity & analysisから設定できます。 このページでDependabot alertsとDependabot security updatesを有効にするだけです。 有効にしておくとセキュリティーアラートがある場合にリポジトリページに下記のように表示されます。 See Dependabot alertsをクリックすると詳細ページに飛べます。 詳細ページからは脆弱性を解消するためのプルリクを生成することができ、とても便利です。 (当然ですが、脆弱性を解消するライブラリのバージョンが存在していない場合はプルリクは生成できません) 下記のようなプルリクが作られるので、内容を確認して問題なければマージしてバージョンアップ完了です dependabot dependabotは脆弱性有無にかかわらず、ライブラリのバージョンアップを検知してプルリクを作ってくれるサービスです。 こちらも入れておきましょう。 marketplaceからインストールします。 インストールするとSettings>Applicationsに追加されます。 設定画面から対象のリポジトリを追加しましょう。 これで新しいバージョンのライブラリを検知した場合、プルリクを勝手に作ってくれます。 CI(GitHub Actions) ここまででRSpecやRuboCopなどをインストールして自動テストや静的解析が整ってきましたが、コードを修正するたびに手動で各種チェックツールを実行するのは現実的ではなく、実行し忘れのリスクがあります。 そこで、Pushしたときに自動実行されるようにします。 CIをするためのツールは様々ありますが、GitHubを使っているのでGitHub Actionsを使います。 Github Actionsの構文は公式ドキュメントをご覧ください。 GitHub ActionsでRailsのCIを行う記事は以前に書いたのでこちらもみていただけるとありがたいです。 今回は自動実行したいツールごとにYAMLを作りました。 Brakeman Brakemanを実行します。 Brakemanは静的解析なのでBrakemanのGemだけインストールしています。 また、DBも必要ないのでDBセッティングもなし。 pathsやpaths-ignoreを設定設定してBrakemanに影響があるファイルが更新された時だけ実行するようにしておくと良いです。 .github/workflows/brakeman.yml name: Brakeman on: push: branches: - 'feature/*' - main paths-ignore: - README.md - Dockerfile - docker-compose.yml - 'spec/**' - 'docs/**' jobs: brakeman: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby 3.0.0 uses: ruby/setup-ruby@v1.66.0 with: ruby-version: 3.0.0 bundler-cache: true - name: run brakeman run: | gem install brakeman brakeman -6 -A -w 1 RSpec RSpecを実行します。 GitHub ActionsでRailsからMySQL8系に接続するにも一手間必要なのですが、詳細は別記事に書いています。 .github/workflows/rspec.yml name: RSpec on: push: branches: - 'feature/*' - main paths-ignore: - README.md - Dockerfile - docker-compose.yml jobs: rspec: runs-on: ubuntu-latest timeout-minutes: 10 env: RAILS_ENV: test DB_HOST: 127.0.0.1 DB_PORT: 33060 services: db: image: mysql:8.0.23 volumes: - mysqlconf.d:/etc/mysql/conf.d ports: - 33060:3306 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes BIND-ADDRESS: 0.0.0.0 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby uses: ruby/setup-ruby@v1.64.1 with: ruby-version: 3.0.0 bundler-cache: true - name: bundle install run: | gem install bundler bundle install --jobs 4 --retry 3 --path vendor/bundle - name: migration run: | bundle exec rails db:create bundle exec rails db:test:prepare - name: run rspec run: bundle exec rspec RuboCop RuboCopを実行します。 RuboCopは静的解析なのでRuboCopのGemだけインストールしています。 また、DBも必要ないのでDBセッティングもなし。 paths-ignoreを.rubocopのExcludeに合わせておくと良いと思います。 .github/workflows/rubocop.yml name: RuboCop on: push: branches: - 'feature/*' - main paths-ignore: - README.md - Dockerfile - docker-compose.yml - 'spec/**' - 'docs/**' - 'db/**' jobs: rubocop: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby uses: ruby/setup-ruby@v1.66.0 with: ruby-version: 3.0.0 bundler-cache: true - name: run rubocop run: | gem install rubocop rubocop Seed seedを実行します。 RuboCopやRSpecのようにコード自体をチェックするものではないのですが、コード修正した時にseedを修正し忘れて壊れてしまうことが多々あるので、CIで正常に実行できるか確認するようにしています。 seedに限らず、コード修正時によく修正漏れしてしまうものがある場合はこのように機械的に検知できるようにしておくと良いです。 .github/workflows/seed.yml name: seed on: push: branches: - 'feature/*' - main paths-ignore: - README.md - Dockerfile - docker-compose.yml jobs: seed: runs-on: ubuntu-latest timeout-minutes: 10 env: RAILS_ENV: development DB_HOST: 127.0.0.1 DB_PORT: 33060 services: db: image: mysql:8.0.23 volumes: - mysqlconf.d:/etc/mysql/conf.d ports: - 33060:3306 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes BIND-ADDRESS: 0.0.0.0 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby uses: ruby/setup-ruby@v1.66.0 with: ruby-version: 3.0.0 bundler-cache: true - name: bundle install run: | gem install bundler bundle install --jobs 4 --retry 3 --path vendor/bundle - name: migration run: | bundle exec rails db:create bundle exec rails db:schema:load - name: run seed_fu run: bundle exec rails db:seed_fu Tbls tblsを実行します。 tblsはREAMEに下記のように書かれている通り、CIと相性が良いです。 tbls is a CI-Friendly tool for document a database, written in Go. ドキュメントに記載されている通りdiffというパラメーターを指定するとDBスキーマとドキュメントの差分を検出してくれます。 これを利用してドキュメントの更新漏れを検知できるようにしています。 .github/workflows/tbls.yml name: Tbls on: push: branches: - 'feature/*' paths: - 'docs/tables/*' - 'db/**' jobs: tbls: runs-on: ubuntu-latest env: RAILS_ENV: development DB_HOST: 127.0.0.1 DB_PORT: 33060 services: db: image: mysql:8.0.23 volumes: - mysqlconf.d:/etc/mysql/conf.d ports: - 33060:3306 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes BIND-ADDRESS: 0.0.0.0 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v2.3.4 - name: Set up Ruby uses: ruby/setup-ruby@v1.66.0 with: ruby-version: 3.0.0 bundler-cache: true - name: apt-get run: | sudo apt-get update sudo apt-get install libmysqlclient-dev jq - name: bundle install run: | gem install bundler bundle install --jobs 4 --retry 3 --path vendor/bundle - name: migration run: | bundle exec rails db:create bundle exec rails db:migrate - name: tbls diff uses: docker://k1low/tbls:latest env: TBLS_DSN: mysql://root:@db:3306/app_development TBLS_DOC_PATH: docs/tables with: args: diff Github Actionsのdependabot Github Actionsのyamlで指定しているライブラリもガンガンバージョンアップしていくので、dependabotで検知できるようしておくと良いです。 dependabotの設定ファイル(yaml)はGitHubのリポジトリーページから生成することができます。 DependenciesタブのDependabotで"Create config file"をクリックしてください。 (執筆時点でBetaと記載されているので今後変わるかもしれません) yamlが生成され、ブラウザ上のエディターでyamlが生成されるので、修正してコミットします。 package-ecosystemのところだけ"github-actions"に修正しました。 特に変更しませんでしたが、チェックする頻度なども指定できます。 指定できるものはドキュメントをご覧ください。 i18n i18nは多言語化対応するための仕組みです。 Railsガイドにも記載されている通りRailsに最初から入っています。 特に多言語化対応しないとしても、エラーメッセージなどの文言はi18nの仕組みを使っておくと一元管理されて便利です。 今回はすでに作っているuserモデルのエラーメッセージを英語/日本語で出し分けられるようにしてみます。 Nameの必須チェックしか実装されておらず面白みがないのでi18nの機能をわかりやすく試すためにカスタムバリデーションも追加しました。 app/models/user.rb class User < ApplicationRecord validates :name, presence: true validate :ng_name private def ng_name errors.add(:name, 'にNGNAMEは使えません') if name == 'NGNAME' end end このまま実行すると下記のようなエラーメッセージが出力されます。 ブランクのメッセージは英語になっており、カスタムエラーのメッセージはハードコーディングした通りに出ていますが項目名がNameになっています。 irb(main):001:0> User.create!(name: '') Traceback (most recent call last): 1: from (irb):1:in `<main>' ActiveRecord::RecordInvalid (Validation failed: Name can't be blank) irb(main):002:0> User.create!(name: 'NGNAME') Traceback (most recent call last): 2: from (irb):1:in `<main>' 1: from (irb):2:in `rescue in <main>' ActiveRecord::RecordInvalid (Validation failed: Name にNGNAMEは使えません) まずは設定を変更して英語と日本語を使えるようにします。 今回は言語を分岐させたい箇所で明示的に指定しようと思っているので、デフォルトはenのままにしています。 config/initializers/locale.rb I18n.config.available_locales = %i[ja en] I18n.default_locale = :en 続いて言語ファイル(ja.ymlやen.yml)を作りますが、最初から作るのは大変なのでrails-i18nにある言語ファイルのyamlをがconfig/localesにコピーしておきます。 コピーしてきたyamlに今回必要な項目を追加し、モデルのバリデーションを使うようにします。 --- a/config/locales/en.yml +++ b/config/locales/en.yml + ng_name: cannot be NGNAME --- a/config/locales/ja.yml +++ b/config/locales/ja.yml ja: activerecord: + attributes: + user: + name: 名前 errors: messages: record_invalid: 'バリデーションに失敗しました: %{errors}' restrict_dependent_destroy: has_one: "%{record}が存在しているので削除できません" has_many: "%{record}が存在しているので削除できません" + ng_name: に'NGNAME'は使えません --- a/app/models/user.rb +++ b/app/models/user.rb def ng_name - errors.add(:name, 'にNGNAMEは使えません') if name == 'NGNAME' + errors.add(:name, :ng_name) if name == 'NGNAME' end end それでは英語、日本語を出し分けてみます。 I18n.with_localeで囲むことでその中で使う言語を変更することができます。 irb(main):001:1* I18n.with_locale(:en) do irb(main):002:1* User.create!(name: '') irb(main):003:0> end Traceback (most recent call last): 2: from (irb):1:in `<main>' 1: from (irb):2:in `block in <main>' ActiveRecord::RecordInvalid (Validation failed: Name can't be blank) irb(main):004:1* I18n.with_locale(:ja) do irb(main):005:1* User.create!(name: '') irb(main):006:0> end Traceback (most recent call last): 3: from (irb):3:in `<main>' 2: from (irb):4:in `rescue in <main>' 1: from (irb):5:in `block in <main>' ActiveRecord::RecordInvalid (バリデーションに失敗しました: 名前を入力してください) irb(main):007:1* I18n.with_locale(:en) do irb(main):008:1* User.create!(name: 'NGNAME') irb(main):009:0> end Traceback (most recent call last): 3: from (irb):6:in `<main>' 2: from (irb):7:in `rescue in <main>' 1: from (irb):8:in `block in <main>' ActiveRecord::RecordInvalid (Validation failed: Name cannot be NGNAME) irb(main):010:1* I18n.with_locale(:ja) do irb(main):011:1* User.create!(name: 'NGNAME') irb(main):012:0> end Traceback (most recent call last): 3: from (irb):9:in `<main>' 2: from (irb):10:in `rescue in <main>' 1: from (irb):11:in `block in <main>' ActiveRecord::RecordInvalid (バリデーションに失敗しました: 名前に'NGNAME'は使えません) config 開発を行っていると、外部環境への接続情報など環境ごとに値を出し分けたいことが多々あります。 そういうときにはconfigというGemを使うと便利です。 これを使うことで環境ごとに切り替えたい設定値をyamlに定義することができるようになります。 Gemfileに追加してインストールします。 インストールコマンドを実行すると、下記のように環境ごとの設定ファイルが生成されます。 config直下のsettingsは環境共通の設定値で、config/settings配下に環境ごとに設定値を設定します。 % docker-compose exec api rails g config:install create config/initializers/config.rb create config/settings.yml create config/settings.local.yml create config/settings create config/settings/development.yml create config/settings/production.yml create config/settings/test.yml append .gitignore また、gitignoreにlocalとついているyamlが除外されるようになっています。 localがついているファイルはgitに保存しないような機密情報を設定するために用意されています。 ただ、特に暗号化されるわけではなくgitignoreに追加されるだけなので管理は自分たちで行う必要があります。 機密情報は次に紹介するcredentialsが使えるのでそちらもご確認ください。 +config/settings.local.yml +config/settings/*.local.yml +config/environments/*.local.yml 早速動作確認をしてみます。 yamlに下記を追加しました。 # config/settings/development.yml app: env: devleopment # config/settings/production.yml app: env: production 下記のように環境ごとに出し分けることができました。 # rails c Loading development environment (Rails 6.1.1) irb(main):001:0> Settings.app.env => "devleopment" # RAILS_ENV=production rails c Loading production environment (Rails 6.1.1) irb(main):001:0> Settings.app.env => "production" Credentials 1つ前に紹介したconfigと同様に環境ごとの設定値を格納できるのですが、こちらは暗号化して保存されます。 暗号化されており値の確認や編集に一手間かかるので機密情報かどうかでconfigと使い分けると良いと思います。 こちらは最初からRailsに入っている仕組みなのでGemの追加は不要です。 早速環境ごとに作ってみます。 下記のようなエラーが発生した場合はEDITORという環境変数に編集に使うエディターを指定して実行ください。 % docker-compose exec api rails credentials:edit --environment development No $EDITOR to open file in. Assign one like this: EDITOR="mate --wait" bin/rails credentials:edit For editors that fork and exit immediately, it's important to pass a wait flag, otherwise the credentials will be saved immediately with no chance to edit. 私はvimを使うのでdocker-compose.ymlの環境変数のところに下記のように追加しました。 docker-compose.yml environment: DB_HOST: db + EDITOR: vim 改めてeditコマンドを実行するとファイルが生成されるので下記の項目を記載しました。 config/credentials/development.yml secret: key: secret!! --environment developmentを指定したのでdevelopment環境用の設定ファイル(暗号化済み)と復号するためのkeyが生成されます。 config/credentials/development.key config/credentials/development.yml.enc またこのときに.gitignoreに/config/credentials/development.keyが追加されます。 keyが流出してしまうと誰でも復号できるようになってしまうので、GitHubには上げずに厳重に管理しましょう。 設定値は下記のように利用することができます。 こちらもSettingsと同様に環境ごとに出し分けることができます。environmentを指定して必要な環境分用意しておきましょう。 irb(main):005:0> Rails.application.credentials.secret[:key] => "secret!!!" Active Job 開発していると実行に時間がかかる処理やリアルタイムで行う必要がない処理などがでてきます。 そのような処理を非同期で実行する仕組みがActive Jobです。 詳細はRailsガイドを参照してください。 Active Jobを使うにはキューとキューに接続するためのフレームワークを使います。 様々なものがありますが、今回は幅広く使われているSidekiqを使いたいと思います。 またキューを保持するためにRedisを使います。 Redis Redisをdocker-composeに追加します。 下記を追加してビルドし直します。PORTは他とかぶらないように少しずらしました。 docker-compose.yml + redis: + image: redis:6.0.9 + ports: + - '6380:6379' 起動できたらコンテナに入って動作確認しておきましょう。 % docker-compose exec redis bash root@86983cd92065:/data# redis-cli -h redis -p 6379 redis:6379> INFO # Server redis_version:6.0.9 ... Sidekiq Sidekiqをインストールします。 ドキュメントが充実しているので参考にします。 まず、Gemfileに追加してbundle installしてRedisの設定をします。 詳細はドキュメントを参照してください。 今回はinitializerを使います。 内容はドキュメント通りですが、テストのときは使わないのでreturnを入れています。 config/initializers/sidekiq.rb return if Rails.env.test? Sidekiq.configure_server do |config| config.redis = { url: 'redis://redis:6379/0' } end Sidekiq.configure_client do |config| config.redis = { url: 'redis://redis:6379/0' } end 続いてActive Jobで使えるようにします。 詳細はドキュメントを参照してください。 テスト以外の環境ではsidekiqを使うのでapplication.rbに追加して、テスト環境だけ:testを設定しました。 config/application.rb config.active_job.queue_adapter = :sidekiq config/environments/test.rb config.active_job.queue_adapter = :test テストアダプターを利用するとジョブを同期実行してくれるようになります。 テストのときに非同期実行されると結果の確認が困難なのでtestの場合はテストアダプターを設定しておくと便利です。 テストジョブ実行 一通り設定は終わったので動作確認用にテストジョブを作って実行します。 動作確認したいだけなのでログを出力するだけのジョブにします。 app/jobs/example_job.rb class ExampleJob < ActiveJob::Base # Set the Queue as Default queue_as :default def perform(*args) Rails.logger.debug 'start job!!!!' end end 続いてsidekiqを起動します。 % docker-compose exec api sidekiq -q default WARN: Unresolved or ambiguous specs during Gem::Specification.reset: minitest (>= 5.1) Available/installed versions of this gem: - 5.14.4 - 5.14.2 racc (~> 1.4) Available/installed versions of this gem: - 1.5.2 - 1.5.1 WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>' Please report a bug if this causes problems. 2021-04-01T08:43:01.144Z pid=38 tid=6hq INFO: Booting Sidekiq 6.2.0 with redis options {:url=>"redis://redis:6379/0"} m, `$b .ss, $$: .,d$ `$$P,d$P' .,md$P"' ,$$$$$b/md$$$P^' .d$$$$$$/$$$P' $$^' `"/$$$' ____ _ _ _ _ $: ,$$: / ___|(_) __| | ___| | _(_) __ _ `b :$$ \___ \| |/ _` |/ _ \ |/ / |/ _` | $$: ___) | | (_| | __/ <| | (_| | $$ |____/|_|\__,_|\___|_|\_\_|\__, | .d$$ |_| 2021-04-01T08:43:02.269Z pid=38 tid=6hq INFO: Booted Rails 6.1.3.1 application in development environment 2021-04-01T08:43:02.269Z pid=38 tid=6hq INFO: Running in ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux] 2021-04-01T08:43:02.270Z pid=38 tid=6hq INFO: See LICENSE and the LGPL-3.0 for licensing details. 2021-04-01T08:43:02.270Z pid=38 tid=6hq INFO: Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org 2021-04-01T08:43:02.273Z pid=38 tid=6hq INFO: Starting processing, hit Ctrl-C to stop 起動したらコンソールからジョブを実行してみます。 irb(main):001:0> ExampleJob.perform_later Enqueued ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) to Sidekiq(default) => #<ExampleJob:0x000056020c20d690 @arguments=[], @job_id="c578b673-7b66-43e4-91ac-d48f34537ccb", @queue_name="default", @priority=nil, @executions=0, @exception_executions={}, @timezone="UTC", @provider_job_id="24ee1022e0a460169097b15a"> 結果はログで確認します。 Sidekiq経由でジョブが実行され、きちんとログが出力されています。 [ActiveJob] Enqueued ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) to Sidekiq(default) [ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] Performing ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) from Sidekiq(default) enqueued at 2021-04-01T08:44:50Z [ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] start job!!!! [ActiveJob] [ExampleJob] [c578b673-7b66-43e4-91ac-d48f34537ccb] Performed ExampleJob (Job ID: c578b673-7b66-43e4-91ac-d48f34537ccb) from Sidekiq(default) in 0.67ms Action Mailer アプリケーションからメールを送信したいことがあると思います。 Railsにはメールを送信する仕組みも入っているので設定しておきます。 詳細はRailsガイドを参照してください。 SendGrid Action Mailerを設定する前にメール送信するためには配信サービスを使う必要があります。 一昔前であれば自分でメールサーバーを立てるなどかなり手間だったのですが、現状はSaasサービスを使ってサクッと配信できるのでそれを利用します。 今回は執筆時点(2021/04)で月に12,000通まで無料でメール配信できるSendGridを利用します。 SendGridは値段がお手頃なだけではなく、Ruby用のGemが用意されているのでRubyとの相性も良いです。 Sidekiq Sidekiqを使ってメール送信を非同期で行うことができます。 ドキュメントに記載されている通りデフォルトではmailersというキューに格納されるのでSidekiqを起動するときにmailersというキューも立ち上げるようにしましょう。 docker-dompose exec api sidekiq -q default -q mailers ちなみにconfig/application.rbなどでconfig.action_mailer.deliver_later_queue_name = 'hoge'のように設定することでキューをmailersから変更することも可能です。 また、nilを設定するとdefaultのキューが使われるようになるようです。 Action Mailerの設定 Action Mailerを設定します。 ActionMailerは配信メソッドをdelivery_methodで指定することができます。 指定できるものはドキュメントを御覧ください。 SendGridをSMTPとして使うだけであれば、delivery_methodにSMTPを設定するだけでできます。 SendGridのドキュメントにもSMTPで設定する方法が記載されています。 ただ、この方法ではSendGridの機能を活かすことができないので、SendGridの機能を活かせるようにdelivery_methodに指定できるカスタムメソッドを作成したいと思います。 Railsではadd_delivery_methodという配信メソッドを追加する仕組みが用意されているため、SendGridを使う配送メソッドを独自に実装してdelivery_methodで使えるようにします。 SendGridの配送メソッド作成 それではSendGridの配送メソッドを作成します。 ゼロから作るのは難しいので、SMTPのソースを参考に作成します。 SMTPのソースをadd_delivery_methodしている箇所 ソースの一部を抜粋しました。 smtpが指定された場合に使うクラスMail::SMTPとパラメーター(address〜enable_starttls_auto)を指定しています。 add_delivery_method :smtp, Mail::SMTP, address: "localhost", port: 25, domain: "localhost.localdomain", user_name: nil, password: nil, authentication: nil, enable_starttls_auto: true SMTPのソース ソースのうち、publicメソッドを抜粋しました。 下記の2つのメソッドが定義されています。 initializeでは、add_delivery_methodで指定した引数を受け取っています。 deliver!では、mailオブジェクトが渡されるので配送処理を実装します。 def initialize(values) self.settings = DEFAULTS.merge(values) end def deliver!(mail) response = start_smtp_session do |smtp| Mail::SMTPConnection.new(:connection => smtp, :return_response => true).deliver!(mail) end settings[:return_response] ? response : self end これらを参考に配送メソッドを作成しました。app/serviseに置きましたが場所はどこでもOKです。 配送処理はsendgrid-rubyのREADMEを参考にしています。 contentを複数指定したかったので、細かいところはmailクラスをのぞいてみて実装しています。 https://github.com/sendgrid/sendgrid-ruby/blob/main/lib/sendgrid/helpers/mail/mail.rb app/services/send_grid_service.rb class SendGridService attr_reader :api_key, :mail def initialize(settings) @api_key = settings[:api_key] end def deliver!(mail) @mail = mail sg = SendGrid::API.new(api_key: api_key) response = sg.client.mail._('send').post(request_body: request_body) raise response.inspect if response.status_code.to_i >= 300 response.body end private def request_body sg_mail = SendGrid::Mail.new sg_mail.from = SendGrid::Email.new(email: mail.from.first) personalization = SendGrid::Personalization.new mail.to.each { personalization.add_to(SendGrid::Email.new(email: _1)) } sg_mail.add_personalization(personalization) sg_mail.subject = mail.subject mail.body.parts.each do content_type = "#{_1.main_type}/#{_1.sub_type}" sg_mail.add_content(SendGrid::Content.new(type: content_type, value: _1.body.raw_source)) end sg_mail.to_json end end 上記の配送クラスをadd_delivery_methodを使って追加します。 api_keyはSendgridで発行したものを使います。環境ごとに違う&機密情報なのでcredentialsを使いました。 ちなみにto_prepareを使うとproductionでは初回のみしか読み込まれませんが、developmentでは都度読み込まれるので開発時に便利です。 config/initializers/sendgrid.rb Rails.configuration.to_prepare { ActionMailer::Base.add_delivery_method(:sendgrid, SendGridService, api_key: Rails.application.credentials.sendgrid_api_key) } 追加できたのでdelivery_methodで指定します。 ついでにconfig.action_mailer.raise_delivery_errors = trueを設定しておくとメール配信時にエラーが発生したときにエラー検知できるようになるので設定しておきましょう。 config/application.rb config.action_mailer.delivery_method = :sendgrid config.action_mailer.raise_delivery_errors = true config/environments/test.rb config.action_mailer.delivery_method = :test テストの場合はメールを送りたくないので:testを指定しておきましょう。 testを指定するとメール配信される代わりにActionMailer::Base.deliveriesに格納されるためテストのときに便利です。 メール配信 設定が終わったのでメールクラスを実装します。 rails g mailerで必要なファイルを生成します。 今回はUserMailerにしました。 % docker-compose exec api rails g mailer UserMailer hello create app/mailers/user_mailer.rb create app/mailers/application_mailer.rb invoke erb create app/views/user_mailer create app/views/layouts/mailer.text.erb create app/views/layouts/mailer.html.erb create app/views/user_mailer/hello.text.erb create app/views/user_mailer/hello.html.erb invoke rspec create spec/mailers/user_mailer_spec.rb create spec/fixtures/user_mailer/hello create spec/mailers/previews/user_mailer_preview.rb テストメールを送りたいだけなのでサクッとmailerクラスとviewを作ります。 viewはRailsガイドに書いてあるとおりhtmlとtext形式を用意しました。 自動生成されたviewファイルに.enをつけていますが、ここを言語の数だけ用意するだけでlocaleの設定によってメールを出し分けてくれます。 今回は動作確認をしたいだけなのでdefault_localeにしているenだけ準備しました。 app/mailers/user_mailer.rb class UserMailer < ApplicationMailer def hello(user_id) @user = User.find user_id # usersテーブルにemailカラム追加 mail(to: user.email, subject: 'Hello!! ham!!' ) end end app/views/user_mailer/hello.en.html.erb hello!! <%= @user.name %>.<br> first mail!!!<br> app/views/user_mailer/hello.en.text.erb hello!! <%= @user.name %>. first mail!!!<br> 準備ができたのでRails consoleを使って動作確認してみます。 irb(main):002:0> UserMailer.hello(1).deliver_now User Load (0.9ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1 Rendering layout layouts/mailer.html.erb Rendering user_mailer/hello.en.html.erb within layouts/mailer Rendered user_mailer/hello.en.html.erb within layouts/mailer (Duration: 0.1ms | Allocations: 6) Rendered layout layouts/mailer.html.erb (Duration: 2.6ms | Allocations: 82) Rendering layout layouts/mailer.text.erb Rendering user_mailer/hello.en.text.erb within layouts/mailer Rendered user_mailer/hello.en.text.erb within layouts/mailer (Duration: 0.1ms | Allocations: 4) Rendered layout layouts/mailer.text.erb (Duration: 3.7ms | Allocations: 80) UserMailer#hello: processed outbound mail in 18.5ms Delivered mail 60730ef18f54d_14c21fc2499b@77f71c6e18a0.mail (623.8ms) ...(中略) <60730ef18f54d_14c21fc2499b@77f71c6e18a0.mail>>, <Subject: Hello!! ham!!>, <Mime-Version: 1.0>, <Content-Type: multipart/alternative; boundary="--==_mimepart_60730ef18e808_14c21fc2489e"; charset=UTF-8>, <Content-Transfer-Encoding: 7bit>> メールが届くことを確認しました。 hello!! ham. first mail!!! また、Sidekiqを起動して非同期で遅れることも確認します。 % docker-compose exec api sidekiq -q default -q mailers 非同期送信 irb(main):003:0> UserMailer.hello(1).deliver_later Enqueued ActionMailer::MailDeliveryJob (Job ID: af499814-b119-4a69-b6db-d37935b5e650) to Sidekiq(mailers) with arguments: "UserMailer", "hello", "deliver_now", {:args=>[1]} => #<ActionMailer::MailDeliveryJob:0x00005651c0d9bbc0 @arguments=["UserMailer", "hello", "deliver_now", {:args=>[1]}], @job_id="af499814-b119-4a69-b6db-d37935b5e650", @queue_name="mailers", @priority=nil, @executions=0, @exception_executions={}, @timezone="UTC", @provider_job_id="88ff5493a88556145308df57"> Sidekiqのコンソール 2021-04-11T15:02:46.229Z pid=395 tid=883 class=ActionMailer::MailDeliveryJob jid=88ff5493a88556145308df57 INFO: start 2021-04-11T15:02:47.251Z pid=395 tid=883 class=ActionMailer::MailDeliveryJob jid=88ff5493a88556145308df57 elapsed=1.054 INFO: done Sidekiqを経由してもメール送信できることを確認できました。 Active Storage アプリケーションからファイルをアップロードしたいことがあると思います。 Railsにはファイルアップロードの仕組みも入っているので設定しておきます。 詳細はRailsガイドを参照してください。 セットアップ Railsガイドに記載されている通り、インストールコマンドを実行します。 % docker-compose api rails active_storage:install 実行すると、Active Storageで使うテーブルを作成するマイグレーションファイルが生成されるので実行しておきます。 config/storage.ymlを参照するとAWSやGCPを保存先にするサンプルがコメントされています。 ここの設定値を環境に合わせて変更することで外部サービスのストレージと簡単に連携することができます。 また、アップロードするファイルに対するバリデーションを行うためのGemもあるためインストールしておきます。 今回は開発環境を構築しているので外部ストレージを使わずローカル保存で動作確認します。 ファイルアップロード実装 Userモデルに画像を添付できるようにしてみます。 active_storage_validationsも入れたので、画像以外のファイルやサイズが4MB以上のファイルはアップロードできないようにバリデーションを追加してみました。 app/models/user.rb has_one_attached :avatar validates :avatar, content_type: %i[png jpg jpeg], size: { less_than: 4.megabytes } 次にアップロードするエンドポイントを追加します。 config/routes.rb - resources :users + resources :users do + post :avatar + end routesを確認してみます。 今回追加したファイルアップロード用のエンドポイントuser_avatar POST /users/:user_id/avatar(.:format)が追加されています。 また、/rails/active_storageで始まるActive Storage用のエンドポイントがいくつか追加されています。 % docker-compose exec api rails routes Prefix Verb URI Pattern Controller#Action user_avatar POST /users/:user_id/avatar(.:format) users#avatar users GET /users(.:format) users#index POST /users(.:format) users#create user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy rails_service_blob GET /rails/active_storage/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_service_blob_proxy GET /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_blob_representation GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_blob_representation_proxy GET /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create アップロード処理を実装します。 受け取ったファイルデータをattachするだけです。とても簡単ですね。 app/controllers/users_controller.rb def avatar user = User.find params[:user_id] user.avatar.attach params[:avatar] raise 'バリデーションエラー' if user.invalid? head :ok end 動作確認 動作確認します。 APIにリクエストを送れるツールなら何でも良いのですが、今回はChromeの拡張機能にあるTalend API Testerを使いました。 下記のようにリクエストして200 OKが返却されました。 rails consoleでファイル添付されていることをattached?で確認して参照用のURLを発行します。 irb(main):001:0> user = User.first irb(main):002:0> user.avatar.attached? => true irb(main):003:0> Rails.application.routes.url_helpers.rails_storage_proxy_path(user.avatar, only_path: true) => "/attachments/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--0efc3d198976ed0b619154371865ef5d0ccf5bfe/avatar.jpg" 早速発行したURLにブラウザでアクセスしてみると、下記のようなエラーが発生しました。 NoMethodError (undefined method `flash' for #<ActionDispatch::Request GET "http://localhost:3001/attachments/blobs/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--0efc3d198976ed0b619154371865ef5d0ccf5bfe/avatar.jpg" for 172.23.0.1>): APIモードだとflashが使えないのでflashが見つからずエラーになっているようです。 下記を追加して使えるようにします。 config/application.rb config.middleware.use ActionDispatch::Flash 再度アクセスすることでファイルを表示することができました。 おまけ 参照URLの/rails/active_storageを変える Active Storageで自動追加されるURLのprefixは/rails/active_storageになります。 システム名が丸見えでこのまま使いたいと思う人は少ないと思います。 変更するための仕組みが用意されているので変更しておきましょう。 config.active_storage.routes_prefix = '/attachments'のように設定することで/rails/active_storageを変更することができます。 上記のように設定すると下記のように変わります。 % docker-compose exec api rails routes rails_service_blob GET /attachments/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_service_blob_proxy GET /attachments/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show GET /attachments/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_blob_representation GET /attachments/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_blob_representation_proxy GET /attachments/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show GET /attachments/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show rails_disk_service GET /attachments/disk/:encoded_key/*filename(.:format) active_storage/disk#show update_rails_disk_service PUT /attachments/disk/:encoded_token(.:format) active_storage/disk#update rails_direct_uploads POST /attachments/direct_uploads(.:format) 使わないエンドポイントを消す Active Storageのエンドポイントがいくつか定義されますが、アプリケーションで全てを使うことはないと思います。 使わないエンドポイントは閉じておきたいので、エンドポイントを消す手順を調べてみました。 config.active_storage.draw_routes = falseを設定することまるごと削除することができるのですが、一部だけ消す方法は見当たりませんでした。 そこで、railsのソースから必要な箇所だけ持ってくることにしました。 まずはconfig.active_storage.draw_routes = falseを設定してデフォルトで追加されるルートを削除します。 config/application.rb config.active_storage.draw_routes = false 次にconfig/routes.rbにRailsのソースから必要な箇所を転記します。 参照で使うエンドポイントだけ残まして他は削除しました。 以下差分です。長くなるのでscope以下は省略しています。 config/routes.rb # # 下記から必要な箇所を転記 # https://github.com/rails/rails/blob/v6.1.3.1/activestorage/config/routes.rb # scope ActiveStorage.routes_prefix do get "/blobs/redirect/:signed_id/*filename" => "active_storage/blobs/redirect#show", as: :rails_service_blob get "/blobs/proxy/:signed_id/*filename" => "active_storage/blobs/proxy#show", as: :rails_service_blob_proxy get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service end ...(長いので省略) routesを確認すると減っていることが確認できます。 % docker-compose exec api rails routes ...(前略) rails_service_blob GET /attachments/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show rails_service_blob_proxy GET /attachments/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show rails_disk_service GET /attachments/disk/:encoded_key/*filename(.:format) active_storage/disk#show 今回はRailsのソースコードをコピペして対応しました。 このように対応するとRailsをバージョンアップしたときなどオリジナルのソースが更新されてしまったときにコピペした部分にも反映が必要となります。 コピペ対応はバージョンアップ時のバグの温床になりやすいので可能な限りやらないほうが良いです。 どうしてもやる場合は、下記のようにコピペした箇所にバージョンが変わった場合にエラーになる処理を追加しておきましょう。 config/routes.rb raise 'バージョンアップしたので下記コピペコードも確認' unless ActiveStorage.version.to_s == '6.1.3.1' これを入れておくとバージョンが変わるとエラーが発生するようになり、バージョンアップ時に反映漏れを防げるようになります。 /app/config/routes.rb:10:in `block in <top (required)>': バージョンアップしたので下記コピペコードも確認 (RuntimeError) その他のおすすめ機能 ここまで私がほぼ外せないと思っている機能を設定してきました。 この章では必須ではないが、便利だと思っている機能を紹介します。 devise deviseはRailsに認証を追加するために便利なgemです。 最近では認証・認可はIDaaSを利用することも多いですが、そこまでお金をかけられない場合にサクッと認証を導入するときに便利です。 deviseにはメール送信する処理もあるのですが、同期処理で送信しています。 deviseのメールを非同期にするgem(devise-sync)もあるので合わせて使うと便利です。 さらに私は使ったことはないですが、deviseに2要素認証を入れるgem(devise-two-factorもあるようです。 最近では多要素認証が主流になってきているのでチェックしておきたいです。 JWT JWTは認証用トークンなどで幅広く使われているトークンです。 Amazon CognitoやAzure ADなどでも使われています。 JWT自体の仕組みについてはこの記事には書きませんが、JWTはトークン自体に様々な情報をもたせることができるのでとても便利です。 Rubyで使うためのGem(ruby-jwt)もあるため簡単に導入することができます。 認証を実装するときは候補の1つとしておくと良いと思います。 graphql-ruby RailsでGraphQLを使うためのGemです。 GraphQLはRESTに変わるAPIのインターフェースで、クエリーを指定することで呼び出し側からレスポンスをカスタマイズすることができます。 詳細は公式ドキュメントを御覧ください。 Rails単体で見ると入出力のバリデーションを自動で行ってくれたり、スキーマファイルが自動生成されスキーマファーストの開発がやりやすいなどメリットがありますが、GraphQLの最大のメリットは様々なプログラミング言語に広がっている点だと思います。 マイクロサービス化が主流となっている昨今ではサービス間でプログラミング言語が異なることがよくあります。 そのため、様々な言語でGraphQLのフレームワークが提供されていることはシステム連携する上で大きなメリットだと思います。 初見ではとっつきにくいとこもありますが、新たにAPIを作成する場合は一度検討してみてください。 開発環境完成 かなり長くなりましたが、ここまでの手順でローカルで動作する開発環境がやっと完成しました。 開発時に便利(というか複数人で開発するときは必須)であるCIの設定や、非同期処理やメール配信、環境ごとの設定変更などプロダクトとして利用するアプリケーションを開発するときにはほぼ100%必要となる機能は一通り入れることができたと思います。 この他にも最後に紹介したdeviseやgraphqlなど開発するプロダクトに合わせて必要なものを追加していきましょう。 今回はプロダクション環境での実行については触れませんでしたが、プロダクションで動かすにはインフラやトラフィックに合わせたチューニングなどがさらに必要になります。 ここまでも長かったですが、まだまだここがプロダクト開発のスタート位置です! 作ったプロダクトを継続的にどんどん育てていきましょう!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

load_documents の利用について

背景 ドットインストールYAML講座にてYAMLのデータを記載し、Rubyファイルを実行しようとしました。 しかし、以下のエラーが発生してしまいます。 Traceback (most recent call last): 2: from parse.rb:3:in `<main>' 1: from parse.rb:3:in `open' parse.rb:4:in `block in <main>': undefined method `load_documents' for Psych:Module (NoMethodError) 何やら load_documents に問題がありそうです。 解決策 調べた結果、現在は load_documents の使用はRuby 2.5 にて廃止されており(ご指摘いただきましたので修正しました)、代わりに load_stream の使用をすればいいとのことでした。load_stream に書き換えて再度実行したらうまくできました。 参照 参考にさせていただきました。ありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

お気に入り機能とルーティングのネストについて

前提 Railsにて、単語帳アプリを作成しました。 登録される単語をWordモデルとし、お気に入り機能を実装するため、Wordモデルと紐づくFavoriteモデルがあり、以下のようなアソシエーションが組まれています。 app/model/word.rb class Word < ApplicationRecord has_many :favorites, dependent: :destroy end app/model/favorite.rb class Favorite < ApplicationRecord belongs_to :word end dependent: :destroyで、親モデルが削除されたときは、それに紐付いている子モデルも削除されるように設定。 単語をお気に入りに追加もしくはお気に入りから外す際には、以下のアクションが働きます。 app/controllers/favorites_controller.rb class FavoritesController < ApplicationController before_action :set_word def create #お気に入りに追加する @favorite = Favorite.create(word_id: @word.id) end def destroy #お気に入りから外す @favorite = Favorite.find_by(word_id: @word.id) @favorite.destroy end private def set_word #パラメーターとして送られてくるword情報をインスタンス変数に代入 @word = Word.find(params[:word_id]) end end 予定している動きとしては、以下の通りです。 「お気に入りに追加」ボタンを押すと、その単語の情報がFavoritesControllerにパラメーターとして送られてくる。 (params(送られてきたパラメーターをハッシュのような構造で格納したもの)によって、設定したルーティングのURLに含まれているword_idがFavoritesコントローラーのcreateアクションへ渡される) before_actionにより、コントローラで定義されたアクション(今回の場合、createアクション)が実行される前に、set_wordアクションが実行され、findメソッドを使用し引数で指定したidのレコードを取得し、インスタンス変数@wordに代入。(引数では、送られてきたパラメーターに含まれるwordのid情報をparams[:キー名]で取り出している) createアクションが実行され、word_idには、パラメーターとして送られてきたwordのidを値に持つインスタンス(お気に入り)を生成し、インスタンス変数@favorieに代入。 この動きを実現させるために、リスト1に関わる、「wordのid情報をパラメーターとしてURLに含める」という作業が必要になります。その上で、コントローラーがその情報を受け取り、アクションが実行され、お気に入りインスタンスが生成されるからです。この場合、ルーティングのネストを意識しなければなりません。 ルーティングのネスト ネストとは入子構造のこと。 まず、ネストを意識せずにルーティングを記述すると以下の通り。 config/routes.rb Rails.application.routes.draw do resources :word resources :favorites, only: [:create, :destroy] end この状態で、ターミナルにて、rails routesを実行します。 ターミナル Prefix Verb URI Pattern Controller#Action favorites POST /favorites(.:format) favorites#create favorite DELETE /favorites/:id(.:format) favorites#destroy これだとパス(URL)の中に、どのwordに対してお気に入り機能が働いているかを示す情報がありません。 お気に入り機能が働く際には、どのワードに対してのものなのかをパスから判断し、かつコントローラーにその情報を渡したいので、ルーティングのネストを用います。 以下のように記述。 config/routes.rb Rails.application.routes.draw do resources :word do resources :favorites, only: [:create, :destroy] end end ターミナルにて、rails routesを実行します。 ターミナル Prefix Verb URI Pattern Controller#Action word_favorites POST /words/:word_id/favorites(.:format) favorites#create word_favorite DELETE /words/:word_id/favorites/:id(.:format) favorites#destroy パスの:word_idという部分に記述された値は、パラメーターとして送られます。 このように、ネストを利用すればwordのid情報を含めることができます。 ルーティングをネストさせる一番の理由は、アソシエーション先のレコードのidをparamsに追加してコントローラーに送るところにあると言えます。 この:word_idの箇所へ、お気に入り機能が働くと結びつくワードのidを記述すると、paramsのなかにword_idというキーでパラメーターが追加され、コントローラーで扱うことができます。 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む