Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby: Ruby 3.3アップデート後に`bin/rails`系のコマンド実行時にconcurrent-rubyでSegmentation faultが発生する件のメモ📝

個人のWebサービスRuby 3.3アップデート後にbin/rails系のコマンド実行時にconcurrent-rubyでSegmentation faultが発生したのでメモ📝

$ bin/rails c

/app/vendor/bundle/ruby/3.3.0/gems/concurrent-ruby-1.2.2/lib/concurrent-ruby/concurrent/atomic/lock_local_var.rb:14: [BUG] Segmentation fault at 0x007effff843e06c0
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [aarch64-linux]

結論としてはRuby v3.3のバグのようだった。(arm64系のCPUを利用していると発生するっぽい?)

bugs.ruby-lang.org

concurrent-ruby側でもrubyの問題として上記のチケットへ誘導するためのissueが立てられている🎫

github.com

masterには対応のPRがマージされておりRuby 3.3.1のリリースで修正されるとのこと🙏

github.com

アップデートはRuby 3.3.1まで待つことにした・・・!

余談) Ruby 3.3のバックポートのリストを見ると、今後リリースされる予定のbug fixとかを見れることを知った📝

bugs.ruby-lang.org

Ruby on Rails: テスト用にrouteを動的に追加するメモ📝

テスト用にRoutingを動的にいじってテストしたいことがたまにあるのでやり方をメモ📝

まず以下のようなテスト用のhelperを用意します。 中でやっていることは動的にrouteを追加するdraw_test_routesとそれをリセットするreload_routes!を実装しています。

module RoutesHelper
  def draw_test_routes(&block)
    Rails.application.routes.disable_clear_and_finalize = true

    Rails.application.routes.draw do
      instance_exec(&block)
    end
  end

  def reload_routes!
    Rails.application.reload_routes!
  end
end

Rails.application.routes.disable_clear_and_finalize = trueをすることで以下のroutesのclearやfinalizeを無効化してroutesを固定化させず、動的に追加したものを初期化させないようにします。

https://github.com/rails/rails/blob/v7.1.2/actionpack/lib/action_dispatch/routing/route_set.rb#L428-L433

またRails.application.reload_routes!を実行することで、clear!し再読み込みされfinalize!することで動的に追加したroutesを削除するとともにRails.application.routes.disable_clear_and_finalizeがfalseに再設定されるので、元に戻ります。

https://github.com/rails/rails/blob/v7.1.2/railties/lib/rails/application/routes_reloader.rb#L22-L29

以下の通り、任意のcontrollerを使って動的にrouteを追加することができました🎉

require "spec_helper"
require_relative "../../test/support/routes_helper"

class TestsController < ActionController::Base
  def index
    head :ok
  end
end

describe "GET /test", type: :request do
  include RoutesHelper

  before(:example) do
    draw_test_routes do
      get "/tests", to: "tests#index"
    end
  end

  after(:example) do
    reload_routes!
  end

  it "success" do
    get "/tests"
    expect(response).to have_http_status :ok
  end
end

参考

techracho.bpsinc.jp

Ruby on Rails: メーラー関連のファイルを`app/mailers`に集約するメモ📝

Ruby on RailsメーラーのViewはデフォルトでapp/views配下に置かれてしまい数が多くなってくるとControllerからrenderされるものとMailerからrenderされるものが混在してしまい見通しが悪くなるケースがあります。

メーラーのビューは app/views/name_of_mailer_class ディレクトリに置かれます。
Action Mailer の基礎 - Railsガイド

そういうわけでメーラー関連のファイルはapp/mailers配下に集約する方法をメモ📝

やり方

目指す姿は以下のような感じで、mailer配下にviewsを持ち、そこにlayoutや各mailerのviewを配置することを目指します。

app/mailers
├── application_mailer.rb
├── sample_mailer.rb
└── views
    ├── layouts
    │   ├── application_mailer.html.erb
    │   └── application_mailer.text.erb
    └── sample_mailer
        ├── sample_mail.html.erb
        └── sample_mail.text.erb

やり方は、そこまで難しくなくて以下のようなApplicationMailerを用意してあげるだけで大丈夫でした!

class ApplicationMailer < ActionMailer::Base
  default from: "from@example.com"
  layout Rails.root.join("app/mailers/views/layouts/application_mailer")
  prepend_view_path "app/mailers/views"
end

layout Rails.root.join("app/mailers/views/layouts/application_mailer")app/mailers内のlayout用のファイルを参照するようにし、

別のレイアウトファイルを明示的に指定したい場合は、メーラーでlayoutを呼び出します。
Action Mailer の基礎 - Railsガイド

prepend_view_path "app/mailers/views"でviewの参照パスにapp/mailers/viewsを追加しています。

prepend_view_pathメソッドやappend_view_pathメソッドを用いることで、パスの解決時に優先して検索される別のディレクトリを追加できます。
Action View の概要 - Railsガイド

参考

techracho.bpsinc.jp

VSCodeの`rebornix.ruby`から`shopify.ruby-lsp`に乗り換えるメモ

今までVSCodeRubyを使うときにはrebornix.rubyを使うことが多かったと思うのですが、

marketplace.visualstudio.com

現状は非推奨となっており、

Shopify's ruby-lsp and associated vscode-ruby-lsp are recommended alternatives to this extension. It is substantially easier to produce a high-quality LSP implementation using Ruby itself vs relying on another language such as TypeScript. GitHub - rubyide/vscode-ruby: Provides Ruby language and debugging support for Visual Studio Code

推奨されているshopify.ruby-lspに乗り換えてみたのでメモ📝

marketplace.visualstudio.com

乗り換えは簡単でVSCode拡張機能からinstallした後にGemfileにruby-lspを追加してbundle installすれば大丈夫でした🙆‍♂️

group :development do
  gem "ruby-lsp"

Gemfileに入れないとエラーになったのでGemfileに入れましたが、公式のReadme的には起動時に.ruby-lsp配下にGemfileが追加されてよしなに利用されるため不要なようです。(自分はなぜか動かなかった・・・)

NOTE: starting with v0.7.0, it is no longer recommended to add the ruby-lsp to the bundle. The gem will generate a custom bundle in .ruby-lsp/Gemfile which is used to identify the versions of dependencies that should be used for the application (e.g.: the correct RuboCop version).
GitHub - Shopify/ruby-lsp: An opinionated language server for Ruby

あとは私はrubocopじゃなくてstarndardrbを利用しているのですが、Ruby LSPが出すrubocopの警告は不要のためdiagnosticsfalseにしました。

  "rubyLsp.enabledFeatures": {
    "diagnostics": false,

これでいい感じにコードジャンプ等が動くようになりました💎

あとrails用のもあるらしい。

github.com

ShopfyこういったRubyの基盤的な技術もOSSで提供してくれていてありがたい🙏

参考)

code.visualstudio.com

Ruby on Rails: 個人のサービスをRails v7.1にアップデートしたのでやったこととかメモ📝

2023/10/05にRuby on Rails v7.1がリリースされました🎉

rubyonrails.org

個人のwebサービスなので以下のように規模はかなり小さめですがやったこととかをメモ📝

+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |    572 |    425 |      21 |      71 |   3 |     3 |
| Helpers              |     51 |     37 |       0 |       8 |   0 |     2 |
| Jobs                 |     31 |     23 |       3 |       2 |   0 |     9 |
| Models               |   1100 |    712 |      32 |     102 |   3 |     4 |
| Mailers              |      6 |      4 |       1 |       0 |   0 |     0 |
| Views                |    393 |    371 |       0 |       0 |   0 |     0 |
| JavaScript           |   3033 |   2195 |       0 |       2 |   0 |  1095 |
| Libraries            |    262 |    214 |       5 |      18 |   3 |     9 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                |   5448 |   3981 |      62 |     203 |   3 |    17 |
+----------------------+--------+--------+---------+---------+-----+-------+

やったこと

installするRailsのバージョンを更新

dependabotのPRが作成されていたので、それを利用しました🤖

bundle update --conservativeを使って最低限のgemだけあげた方が安全。

--conservative Use bundle install conservative update behavior and do not allow indirect dependencies to be updated. https://bundler.io/v2.4/man/bundle-update.1.html

設定周りをRails v7.1に合わせて更新

config.load_defaults 7.1に更新してRails 7.1の設定を反映するようにしたのと、 Rails v7.1時のRails newのdiffを確認し以下の変更を追加した。

https://railsdiff.org/7.0.8/7.1.0

config.autoload_libのignoreオプションを利用するように修正

config.autoload_lib(ignore:)という新しい設定メソッドが導入されました。 このメソッドは、デフォルトでは自動読み込みパスに含まれていないlibディレクトリをアプリケーションの自動読み込みパスに追加するために利用されます。 また、新しいアプリケーションではconfig.autoload_lib(ignore: %w(assets tasks))が生成されます。 https://railsguides.jp/v7.1/7_1_release_notes.html

config.cache_classesconfig.enable_reloadingに変更

3.2.13 config.cache_classes 後方互換性のためにサポートされている古い設定であり、!config.enable_reloadingと同等です。 https://railsguides.jp/configuring.html

config.action_controller.raise_on_missing_callback_actionsをtrueに設定

onlyやunlessで既存のメソッドに存在しないシンボルを指定した場合にコールバックでエラーを発生するようになった。 https://techracho.bpsinc.jp/hachi8833/2023_09_27/135156

config.active_job.verbose_enqueue_logsをtrueに設定

3.15.8 config.active_job.verbose_enqueue_logs バックグラウンドジョブをエンキューするメソッドのソースコードの場所を、 関連するエンキューログ行の下にログ出力するかどうかを指定します。 デフォルトは、development環境ではtrue、それ以外の環境ではfalseです。 https://railsguides.jp/configuring.html

config.action_dispatch.show_exceptions:rescuableに設定

Deprecate true and false values for config.action_dispatch.show_exceptions in favor of :all, :rescuable, or :none. https://edgeguides.rubyonrails.org/7_1_release_notes.html#action-pack-deprecations

警告が出ている部分のFIX

アプリケーション起動時に以下のような警告が出ていましたが、どちらもDevise起因ぽかったので一旦無視しました。

DEPRECATION WARNING: DeprecatedConstantAccessor.deprecate_constant without a deprecator is deprecated (called from <top (required)> at /app/config/application.rb:16)
DEPRECATION WARNING: `Rails.application.secrets` is deprecated in favor of `Rails.application.credentials` and will be removed in Rails 7.2. (called from <top (required)> at /app/config/environment.rb:7)

DeprecatedConstantAccessor.deprecate_constant側はissueが上がっている模様。

github.com

DEPRECATION WARNINGの調査はconfig.active_support.deprecation = :raise にするとスタックトレースが出るので発生箇所が調査しやすい。

おわりに

Rails v7.1はasync queriesや複数主キーのサポートなどいい感じの機能が楽しみですね✨

参考

techracho.bpsinc.jp

inside.pixiv.blog

Ruby on Rails: Content Security Policyを使ってみたのでMEMO

Ruby on Rails 5.2からContent Security Policyヘッダーを設定するDSLが提供されました。

2.5 Content Security Policy
Rails 5.2 ships with a new DSL that allows you to configure a Content Security Policy for your application. You can configure a global default policy and then override it on a per-resource basis and even use lambdas to inject per-request values into the header such as account subdomains in a multi-tenant application. You can read more about this in the Securing Rails Applications guide.
Ruby on Rails 5.2 Release Notes — Ruby on Rails Guides

今更ながら個人のサービスに適用してみたので使い方とかをメモ📝

Content Security Policyとは?

HTTP の Content-Security-Policy レスポンスヘッダーは、ウェブサイト管理者が、あるページにユーザーエージェントが読み込みを許可されたリソースを管理できるようにします。いくつかの例外を除いて、大半のポリシーにはサーバーオリジンとスクリプトエンドポイントの指定を含んでいます。これはクロスサイトスクリプティング攻撃 (クロスサイトスクリプティング) を防ぐのに役立ちます。 Content-Security-Policy - HTTP | MDN

上述の通りレンスポンスヘッダーでクライアントがロードするリソースの許可条件を設定できる機能ようです。

CSP を有効にするには、ウェブサーバーから Content-Security-Policy HTTP ヘッダーを返すように設定する必要があります 他にも、 要素を用いてポリシーを指定することも可能です。例を挙げます。 コンテンツセキュリティポリシー (CSP) - HTTP | MDN

設定するにはHTTPヘッダーまたはmetaタグでポリシーに記述することで行えます。

# HTTPヘッダーでContent-Security-Policyを返すようにする
Content-Security-Policy: default-src 'self'
# or metaタグを設定する
<meta http-equiv="Content-Security-Policy" content="default-src 'self' />

Ruby on RailsでContent Security Policyを設定する

Ruby on RailsではDSLを使ってContent-Security-Policy HTTP ヘッダーの内容及びCSP関連の振る舞いを指定することができます。

Rails.application.configure do
  config.content_security_policy do |policy|
    policy.default_src :self, :https # 同一オリジン OR httpsで取得するリソースのみ許可
    policy.script_src :strict_dynamic # nonceにより検証が成功したリソースのみを許可
  end

  # Generate session nonces for permitted importmap and inline scripts
  config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # session_idをnonceの取得元として使用する
  config.content_security_policy_nonce_directives = %w[script-src] # JavaScriptはnonceによる検証の対象にする

  # Report CSP violations to a specified URI. See:
  # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
  config.content_security_policy_report_only = true # CSP違反があってもエラーとしない
end

cssstrict_dynamicにしたい場合にはpolicy.style_src :strict_dynamicにし、content_security_policy_nonce_directives = %w[script-src style-src]を指定するようにすれば良い。 ※ Viteを利用している場合には制約上、script_src: unsafe-eval, style_src: unsafe-inlineを許可する必要がある(?)ようです Link

nonceの付与はjavascript_include_tag等であればnonce: trueを指定すれば自動的に付与できますが、 scriptタグを直接記載しているときにはcontent_security_policy_noncenonceを取得することもできるので直接指定することもできます。

<script nonce="<%= content_security_policy_nonce %>">

おわりに

Ruby on Rails、こういうのを簡単に指定できて非常に便利ですね 🚃

参考

zenn.dev

railsguides.jp

Ruby on Rails: KombuというRuby on Railsでjsのcomponentを指定してrenderできるgemをリリースしました。

タイトル通り、KombuというRuby on Railsjavascriptで描画するcomponentを指定してrenderできるgemをリリースしました💎✨ (コンポーネント・レンダラブルを略してコンブ です)

github.com

モチベーション

Ruby on Railsを利用してサービスが成長してくるとVue.jsといったフロントエンドライブラリを利用することになると思いますが、 これがだんだん成長してくるとRailsのViewとフロントエンドフレームワークがどんどん密結合になっていき、 見通しが悪くなったり適切な境界を持たないことによるE2Eテストの肥大化及び明示的な検証が困難といった課題が出てくるなぁと思っていたので、 以下の記事に書いたようなControllerから直接ComponentをrenederすることでRailsのView層を隠蔽し一定の境界を手軽に作れるのでは?という感じで作りました。

madogiwa0124.hatenablog.com

似たようなライブラリだと以下のようなものがありますが、React-Railsの場合はReactを採用してないと採用できないのとInertia.js Rails Adapterはinertia.jsを導入しないと採用できず境界を分けたいだけのニーズではtoo muchかなと思い作成に至った次第です。(Vue.jsでの使用を主に想定してします)

使い方

使い方は割と簡単で以下をGemfileに追加 + bundle installして

gem "kombu"

以下の通りに自身のアプリケーションに合わせて、jsからmountする要素のid、css/jsの読み込み用のタグの生成ロジック、隠蔽するview templateを設定して

Rails.application.configure do
  # NOTE: (OPTIONAL) id of the element (div) to mount (default: `vue-root`)
  # config.kombu.default_mount_element_id = 'vue-root'

  # NOTE: (REQUIIRED) Specify a proc that generates a tag that reads a javascript entry.
  # See `lib/kombu/renderable.rb` for instance variables provided by kombu that can be used within proc.
  config.kombu.javascript_entry_tag_proc = -> { helpers.javascript_pack_tag(@entry, defer: true) }

  # NOTE: (REQUIIRED) Specify a proc that generates a tag that reads a css entry.
  # See `lib/kombu/renderable.rb` for instance variables provided by kombu that can be used within proc.
  config.kombu.stylesheet_entry_tag_proc = -> { helpers.stylesheet_pack_tag(@entry) }

  # NOTE: (OPTIONAL) template of the view to render that contains the component. (default: See below)
  # config.kombu.default_entry_view_template = <<~ERB
  #   <div id="<%= @kombu_mount_element_id %>">
  #     <%= kombu_component_tag %>
  #   </div>
  #   <% content_for :stylesheet do %>
  #     <%= kombu_stylesheet_entry_tag %>
  #   <% end %>
  #   <% content_for :javascript do %>
  #     <%= kombu_javascript_entry_tag %>
  #   <% end %>
  # ERB
end

任意のControllerでKombu::Renderableをincludeするとkombu_render_componentを使って任意のComponentを含んだhtmlタグをrenderすることが出来ます。renderする際にViewを必要としないので、layouts以外のViewファイルを削除することが可能になり、フロントエンドとサーバーサイドの境界を作ることが出来ます。

class ArticlesController < ApplicationController
  include Kombu::Renderable

  def index
    @title = "Articles"
    @articles = [{id: 1, title: "artile1", body: "body1"}, {id: 2, title: "artile2", body: "body2"}]
    kombu_render_component("article-index-page", attributes: {title: @title, ":articles": @articles.to_json})
    # NOTE: The following html is rendered.
    # <div id="vue-root"><artile-index-page title="Articles" :articles="[{"id":1,"title":"artile1","body":"body1"},{"id":2,"title":"artile2","body":"body2"}]"></div>
  end
end

またkombu_render_componentに渡された値を明示的に検証できるRSpecのmatcherを用意しているのでrequest specでサーバーサイドから渡す値を明示的に検証することが出来ます🙆‍♂️

describe "GET /articles", type: :request do
  before { get articles_path }

  it "Specific arguments must be passed to kombu_render_component." do
    title = "Articles"
    articles = [{id: 1, title: "artile1", body: "body1"}, {id: 2, title: "artile2", body: "body2"}]
    expect(controller).to kombu_component_rendered("article-index-page", attributes: {title: title, ":articles": articles.to_json})
  end
end

仕組み

基本的にはkombu_component_renderedで指定した名称とattiributesを持つタグとjavascript_entry_tag_proc、stylesheet_entry_tag_procで生成したタグをconfig.kombu.default_entry_view_template`で指定したtemplateに埋め込んで返却しているだけです🧑‍🏭

詳しくはこちらの該当コードを参照してください。

github.com

おわりに

実際に自分の個人サービスで利用してるのですがapp/views配下がlayouts以外消せてスッキリでした👍