Railsアプリを型チェックできるようにSteepに入門してみた
今年も残り少なくなってきて、そういえばRailsアプリに型チェックを導入してみたいなーと思っていたのを思い出したのでSteepに入門してみた。
RailsアプリへのSteep導入は神速さんのブログがすごく丁寧だったのでそれを参考にしてやった sinsoku.hatenablog.com
差分としては
- lib/tasks/rbs.rake を作成する
- 現在は
bin/rails g rbs_rails:install
するとlib/tasks/rbs.rakeが生成される
- 現在は
- 不足してるrbsを用意する
- 自分は
ActionCable::Channel::Base
とActionCable::Connection::Base
だけでよかった
- 自分は
# sig/patch.rbs module ActionCable module Channel class Base end end module Connection class Base end end end
あと、神速さんのブログのscaffold を試すのところにあるようにコントローラーのエラーも確かに発生していて、自分は scaffold userをしたんだけどそのエラーが下記。
# Type checking files: ..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................F..... app/controllers/users_controller.rb:7:4: [error] Cannot find the declaration of instance variable: `@users` │ Diagnostic ID: Ruby::UnknownInstanceVariable │ └ @users = User.all ~~~~~~ app/controllers/users_controller.rb:17:4: [error] Cannot find the declaration of instance variable: `@user` │ Diagnostic ID: Ruby::UnknownInstanceVariable │ └ @user = User.new ~~~~~ app/controllers/users_controller.rb:27:4: [error] Cannot find the declaration of instance variable: `@user` │ Diagnostic ID: Ruby::UnknownInstanceVariable │ └ @user = User.new(user_params) ~~~~~ app/controllers/users_controller.rb:31:34: [error] Type `::UsersController` does not have method `user_url` │ Diagnostic ID: Ruby::NoMethod │ └ format.html { redirect_to user_url(@user), notice: "User was successfully created." } ~~~~~~~~ app/controllers/users_controller.rb:45:34: [error] Type `::UsersController` does not have method `user_url` │ Diagnostic ID: Ruby::NoMethod │ └ format.html { redirect_to user_url(@user), notice: "User was successfully updated." } ~~~~~~~~ app/controllers/users_controller.rb:60:32: [error] Type `::UsersController` does not have method `users_url` │ Diagnostic ID: Ruby::NoMethod │ └ format.html { redirect_to users_url, notice: "User was successfully destroyed." } ~~~~~~~~~ app/controllers/users_controller.rb:69:4: [error] Cannot find the declaration of instance variable: `@user` │ Diagnostic ID: Ruby::UnknownInstanceVariable │ └ @user = User.find(params[:id]) ~~~~~ Detected 7 problems from 1 file
path helperのやつとインスタンス変数の定義がないと怒られたので解決してみた。
path helper関連
これはrbs_rails:allをしたときにsig/rbs_rails/path_helpers.rbsが生成されているのになんでそれ見てくれないのかなと疑問だったんだけど、
# sig/rbs_rails/path_helpers.rbs interface _RbsRailsPathHelpers ・・・ def users_url: (*untyped) -> String def new_user_url: (*untyped) -> String def edit_user_url: (*untyped) -> String def user_url: (*untyped) -> String ・・・ end
このようにinterfaceで定義されているだけなので、これをコントローラーのrbsファイルでincludeしてやればよさそう。
(まだRBSが全く分かってないんだけど、RBS基礎文法最速マスター - pockestrap見つつこう使うのかなーみたいな感じでやってみたらエラーはでなくなった)
# sig/app/controllers/users_controller.rbs class UsersController < ApplicationController include _RbsRailsPathHelpers # GET /users or /users.json def index: () -> untyped # GET /users/1 or /users/1.json def show: () -> nil # GET /users/new def new: () -> untyped ・・・ end
インスタンス変数
Cannot find the declaration of instance variable: `@user` Cannot find the declaration of instance variable: `@users`
@userと@usersのRBS定義がないのでこれは手動で定義した。
# sig/app/controllers/users_controller.rbs class UsersController < ApplicationController @user: User @users: User::ActiveRecord_Relation include _RbsRailsPathHelpers # GET /users or /users.json def index: () -> untyped # GET /users/1 or /users/1.json def show: () -> nil # GET /users/new def new: () -> untyped ・・・ end
あと注意点としては rbs_rails:allしたときにmodelの定義も生成してくれるけど、それはdb:migrateを実行しておかないと生成されない。
rbs_rails/rake_task.rb at 227285bd04d526525331a9b05fe9ce5e78038946 · pocke/rbs_rails · GitHub
::ActiveRecord::Base.descendants.each do |klass| next unless RbsRails::ActiveRecord.generatable?(klass)
ここの RbsRails::ActiveRecord.generatable?(klass)
が
rbs_rails/active_record.rb at 227285bd04d526525331a9b05fe9ce5e78038946 · pocke/rbs_rails · GitHub
def self.generatable?(klass) return false if klass.abstract_class? klass.connection.table_exists?(klass.table_name) end
とあるようにDBにテーブルが存在しているかを見てるので。
これでとりあえずはsteep checkが通るようになったので、なんとか入門までは行けた感じがする。
$ bundle exec steep check # Type checking files: ........................................................................................................................................................................................................................................ No type error detected. 🫖
ActiveJob::TestHelperでセットされるTestAdapterを無効にしたい場合
下記のように ActiveJob::TestHelper
をincludeしているとqueue_adapterが強制的にTestAdapterになります。
[spec/rails_helper.rb] RSpec.configure do |config| config.include ActiveJob::TestHelper end
RSpecのJobのテストとかを書く分には便利なアサーションメソッドとかも使えて便利なのですが、本当にJobを実行したい場合もあります。 今回はActionCableのChannelでJob.perform_laterしてそのジョブで処理が完了したらbroadcastするようなものだったので、同期ではなく非同期でJobを実行したかったです。
初めは Rails.application.config.active_job.queue_adapter = :async
としてqueue_adapterを切り替えようとしていたのですが、どうもTestAdapterから切り替わっていないようでした。(設定直後くらいでbinding.pryして止めても確かにAsyncAdapterにはなっている)
どうにかして部分的にAsyncAdapterに切り替えたいなと調べていると下記を見つけました。 github.com
disable_test_adapterというメソッドを呼ぶことでActiveJob::TestHelperでセットされるTestAdapterを無効にできるようです。 Rails6になった頃、config/environments/test.rbのqueue_adapter設定が無視されるのがあったんですね。
これでasyncアダプターに切り替えて非同期でJobを実行できるようになりました。
補足
同期実行で問題ない場合は下記で見つけた perform_enqueued_jobs
を利用すると同期実行にできます。
qiita.com
RailsでPostgreSQLのVIEW定義を管理する
初めてちゃんとPostgreSQLのVIEWを使うので、Railsで使う場合の調査。
下記のgemを利用するのが定番みたい。
このgem使うと何が嬉しいのか
RailsガイドにもCREATE VIEWする方法が書かれてる。
gem利用しないとCREATE VIEWする生SQLをexecuteするだけなので
- migrationのdownを自分で定義する必要がある
- はじめのCREATE VIEWならdropすればいいけど以降の変更時はその前のやつに戻す必要がある
- レビュー時に以前のmigrationを参照しないと行けないので手間が増える
- はじめのCREATE VIEWならdropすればいいけど以降の変更時はその前のやつに戻す必要がある
マテリアライズドビューを利用するならrefreshの仕組みを用意してくれるのでさらに便利そう。
という感じかな。 あとはおまけとしてエディタでSQLのシンタックスハイライトが効くので見やすいくらいですかね。
update_viewとreplace_viewはどう使い分けるのか
コメントにちゃんと書いてくれてるの助かる。
scenic/postgres.rb at 3600a485797fe1dbf30d152cc60b6d2318f04c48 · scenic-views/scenic · GitHub
CREATE OR REPLACE VIEW
は、既存のビュースキーマの最後に新しいカラムを追加する場合にのみ許可されています。既存のカラムは、順番を変えたり、削除したり、型を変更することはできません [DeepLでの翻訳より]
なので、基本的にはupdate_view使う感じですね。
あとはTechRachoも目を通しておくと良さげ
AWS Batch(AmazonLinux1)のボリュームサイズを増やす時はDockerのストレージ制限もお忘れなく
マネージド型コンピューティング環境(EC2)(AmazonLinux1)のAWS Batchのお話です。
AWS BatchでNo space left on device
でエラーになった場合はディスク容量を増やせばとりあえずはいいのですが、EBSのサイズアップだけでなくDockerのストレージ制限もサイズアップしないとダメです。
下記2つのドキュメントをちゃんと読めば確かに今なら理解できるけど、当時はとりあえずEBSのサイズ増やせばいいんでしょ?って感じで斜め読みしてよく分かってなかった。。。
AWS Batch の「No space left on device」エラーを解決する
起動テンプレートを使用して、AWS Batch の Amazon EBS ボリュームのサイズを増やす
これでEBSのサイズ増やしてもNo space left on device
になって??ってなっていたけど、下記の記事でようやく理解した。
https://bedford.io/projects/cli/doc/aws-batch.html のDisk space for your jobsの項
By default, your Batch jobs will each have access to 10 GiB of scratch space. This is enough for most Nextstrain builds, but yours may require more.
Configuring more space requires a little bit of setup. It also helps to understand that Batch uses ECS to run containers on clusters of EC2 servers.
Each EC2 instance in an ECS cluster has a storage volume named /dev/xvdcz which is shared by all the containers/jobs running on that instance. The default size of this volume is 22 GiB, which comes from the AWS-managed ECS-optimized machine images (AMIs) used by Batch.
Each container/job is allowed to use up to 10 GiB of disk by default. This comes from Docker’s dm.basesize option.
In order to give your Batch jobs more disk, you have to increase both of these defaults. There are many approaches to doing this, but the simplest is to create a new EC2 launch template.
It’s quickest to create the launch template using the AWS Console, although you can also do it on the command-line.
First, add to the launch template an EBS storage volume with the device name /dev/xvdcz, volume size you want (e.g. 200 GiB), and a volume type of gp2. Make sure that the volume is marked for deletion on instance termination, or you’ll end up paying for old volumes indefinitely! This sets the size of the shared volume available to all containers on a single EC2 instance.
Next, under the “Advanced details” section, add a user data blob with the following text:
まとめると
- ECSクラスター内の各EC2インスタンスには/dev/xvdczという名前のストレージボリュームがあり、そのインスタンス上で動作するすべてのコンテナ/ジョブで共有される
- このボリュームのデフォルトサイズは22GiB
- 各コンテナ/ジョブは、デフォルトでは最大10GiBのディスクを使用することができる
- これはDockerのdm.basesizeオプションによるもの
- もっと多くのディスクを割り当てたい場合はEBSとDockerストレージ制限の両方を増やす必要がある
- そのシンプルな方法がEC2起動テンプレートを作ること
ちなみに/dev/xvdcz
はAmazon Linux 1の場合でAmazon Linux 2だと/dev/xvda
しかないようです。
※起動テンプレートを使用して、AWS Batch の Amazon EBS ボリュームのサイズを増やす より
注: 起動テンプレートの DeviceName は、ご利用中の Amazon Elastic Container Service (Amazon ECS) に最適化された Amazon マシンイメージ (AMI) バージョンによって異なる場合があります。デフォルトでは、AWS Batch は Amazon Linux 1 に基づいており、2 つの Amazon EBS ボリューム (/dev/xvda および /dev/xvdcz) が付属しています。Amazon Linux 2 を使用している場合、AWS Batch には 1 つのボリューム (/dev/xvda) のみがあります。詳細については、AMI ストレージ設定をご参照ください。
Amazon Linux2の場合は/dev/xvda
のサイズを増やせばDockerも増えるみたいです。
Amazon Linux2へのアップグレードもちゃんとやらないとな。。。
プライベートなS3バケットにあるファイルをCloudFront経由でアクセスできるようにする
なにかの度にやることがあるけど、スッと設定できずに403エラーになるのでメモで残しておく
まず、下記にあるように静的ウェブサイトホスティングとは設定が異なるので見るページ間違えないこと
CloudFront ディストリビューションを使用して Amazon S3 バケットへのアクセスを制限する
Access Denied エラーをトラブルシューティングするには、ディストリビューションのオリジンドメイン名が S3 ウェブサイトのエンドポイントか S3 REST API エンドポイントかを確認してください。
今回は静的ウェブサイトホスティングはOFFの本当にプライベートなS3バケットにあるファイルをCloudFrontから配信できるようにしたいので、見るべきドキュメントはこっち。
CloudFront ディストリビューションを使用して Amazon S3 バケットへのアクセスを制限する
あとは大体CloudFrontのコンソールでポチポチ画面から設定しているとできるけど、すでにバケットポリシーとかあるとそれが邪魔することがある
6.バケットポリシーの中で、"Effect": "Deny" が含まれる (これにより、CloudFront OAI からバケットへのアクセスを禁止しています) ステートメントを探します。CloudFront OAI がバケット内のオブジェクトにアクセスできるように、これらのステートメントを変更します。
7.バケットポリシーの中で、"Effect": "Allow" が含まれる (これにより、CloudFront OAI 以外のソースからのバケットへのアクセスを許可しています) ステートメントを探します。ユースケースの必要に応じてこれらのステートメントを変更します。
自分は下記みたいなバケットポリシーになっていて、Sid:1の方を消したらCloudFront経由でアクセスできるようになった
{ "Version": "2012-10-17", "Statement": [ { "Sid": "1", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*" }, // CloudFrontのコンソールでやると足されるやつ { "Sid": "2", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity xxxxxxxxxxxxxx" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*" } ] }
RailsでenumerizeのカラムをSTIとして使う
これのenumerize版。
既存のモデルを分けたかったので、参考にさせてもらった。
すでにenumerizeの利用しているカラムをSTIのカラムにしたかったので、inheritance_column
でのSTIのデフォルトカラム(type)を変更もした。
class Payment < ActiveRecord::Base extend Enumerize self.inheritance_column = "pay_type" enumerize :pay_type, in: { credit_card: 0, bank_transfer: 10, paypal: 20 }, scope: true, predicates: true class << self def find_sti_class(type) type_name = pay_type.find_value(type).camelize "Payment::#{type_name.to_s.camelize}".constantize end def sti_name pay_type.find_value(name.demodulize.underscore).value end end end
sti_name
メソッドの定義もなんでいるのかと思ったのですが、Payment::CreditCard.new
とした時にinheritance_column
で定義したカラムに期待する値がちゃんと入るようにしないとなのでちゃんと定義しましょう。 (ここで使われるので)
Goでの日付変換
最近やっとのことGoを真面目にやりだしてTwitterのAPIで取得したtweet.CreatedAtの日時文字列をISO8601形式の文字列にしたいなと思ったけど、time.parse
のformatの指定とかが独特で少しハマった。
Rubyとかだと%Y/%m/%d
とかでformat指定するけど、Goだと2006-01-02
みたいな具体的な年月日などの数字を指定する必要がある。
初めなんでサンプルとかこんな中途半端な日時なんだろう?と思い実装した日に変えたろって変えたら0001/01/01 00:00:00 +0000 UTC
みたいになってしまった。
調べてみると下記のように
GoのtimeのFormat表記方法でハマったこと - emahiro/b.log
結論としては goでのtimeのformat変換では使える文字が決まっている っていうことになります。
と、ありあの日時が固定らしいと分かりなんじゃそりゃ!っという感じ。
なんか特別な意味でもあるのかと思ったけど、下記を見てただふーーーん...というお気持ちになっただけなのでした。
Goのtimeパッケージのリファレンスタイム(2006年1月2日)は何の日? - Qiita