- 投稿日:2019-01-27T22:43:11+09:00
RubyでURLエンコーディングする方法
自分のブログの転載記事です。
結論
URLの中で使用する文字列をエスケープするときには
URI.encode_www_formメソッドを使用しよう。調べたこと
最近、株価を分析するwebアプリを個人開発しています。
その中で、銘柄名でGoogle検索したときの結果をスクレイピングしたくなりました。銘柄名と証券コードはStringでDBに登録済みなので、簡単に持ってこられます。
ですが持ってきた文字列をそのままクエリストリングに入れてしまうと、うまく検索できないことがありました。search_url = "https://www.google.co.jp/search?hl=jp&gl=JP&q=日本M&Aセンター"
欲しい結果は取得できていますが、検索ワードが"日本M"で途切れています。
"&"が正しくエスケープできていないのかと思い、調べてみたところ、以下の方法でできました。require 'uri' query = URI.encode_www_form(q: '日本M&Aセンター') => "q=%E6%97%A5%E6%9C%ACM%26A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC" search_url = "https://www.google.co.jp/search?hl=jp&gl=JP&" => "https://www.google.co.jp/search?hl=jp&gl=JP&" search_url += query => "https://www.google.co.jp/search?hl=jp&gl=JP&q=%E6%97%A5%E6%9C%ACM%26A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC"
上記のURLを打ち込むと、"&"が正しくエスケープされているのがわかります。他の文字はエスケープ前の文字に戻っていますが。失敗したケース
ちなみに調べている中で最初に出てきた方法は
URI.encodeを使う方法だったのですが、そのやり方では正しくエスケープされませんでした。require 'uri' search_url = "https://www.google.co.jp/search?hl=jp&gl=JP&q=" => "https://www.google.co.jp/search?hl=jp&gl=JP&q=" search_url += "日本M&Aセンター" => "https://www.google.co.jp/search?hl=jp&gl=JP&q=日本M&Aセンター" URI.encode search_url => "https://www.google.co.jp/search?hl=jp&gl=JP&q=%E6%97%A5%E6%9C%ACM&A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC"最終的に生成された文字列を見比べてみると、"&"がエスケープされていないことがわかります。
# "M&A"が"M%26A"になっている。 success_url = "https://www.google.co.jp/search?hl=jp&gl=JP&q=%E6%97%A5%E6%9C%ACM%26A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC" # "M&A"が"M&A"のまま。 failure_url = "https://www.google.co.jp/search?hl=jp&gl=JP&q=%E6%97%A5%E6%9C%ACM&A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC"Ruby 2.6.0 リファレンスマニュアルによると、
encodeメソッドはobsoleteとのことなので、今後は使用しないほうがよいかもしれません。
- 投稿日:2019-01-27T20:33:40+09:00
[Ruby] irbで、矢印キーを押すと`^[[D`などと出てしまう現象の解決法
環境
- macOS 10.13.6
- Ruby 2.5.3
現象
irbで矢印キーを押すと、次のような記号が現れてしまうようになってしまいました。
たとえば、文字を修正しようと←キーを押すと…
こうなってしまいます。原因
調べてみると、
irbではreadlineというライブラリを使用していることが分かりました。
しかし、require "readline"を叩いてみてもirb(main):001:0> require "readline" Traceback (most recent call last): 4: from /Users/<username>/.rbenv/versions/2.5.3/bin/irb:11:in `<main>' 3: from (irb):1 2: from /Users/<username>/.rbenv/versions/2.5.3/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require' 1: from /Users/<username>/.rbenv/versions/2.5.3/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require' LoadError (dlopen(/Users/<username>/.rbenv/versions/2.5.3/lib/ruby/2.5.0/x86_64-darwin17/readline.bundle, 9): Library not loaded: /usr/local/opt/readline/lib/libreadline.7.dylib) Referenced from: /Users/<username>/.rbenv/versions/2.5.3/lib/ruby/2.5.0/x86_64-darwin17/readline.bundle Reason: image not found - /Users/<username>/.rbenv/versions/2.5.3/lib/ruby/2.5.0/x86_64-darwin17/readline.bundle…となってしまいます。どうやら
readlineはネイティブなライブラリで、本体のバージョンが変わってしまったために、読み込めないようです。対処法
/usr/local/opt/readline/lib/libreadline.7.dylibがないとのことなので、当該フォルダを見てみると、libreadline.8.dylibというエイリアスがありました。
これを複製し、libreadline.7.dylibとリネームすることで、無事に解決しました。
- 投稿日:2019-01-27T16:46:43+09:00
RailsのActionMailerで画像のURLを絶対パスで表示させる
ActionMailerで送信したメールに絶対パスで画像を表示させたい中でハマった。
Mail内で以下のように表示させるために
index.html<img src="http://myfullappurl.dev/assets/myimage.png">development.rbにて以下のようにメーラーを設定
development.rbconfig.action_controller.asset_host = 'myfullappurl.dev' config.action_mailer.asset_host = config.action_controller.asset_host config.action_mailer.default_url_options = { host: 'myfullappurl.dev' }しかしmail内でのhtmlでは
index.html<img src="//myfullappurl.dev/assets/myimage.png">とプロトコルが表示されない。railsコードに
asset_url_helper.rbURI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}有効なActionMailer URIを定義する正規表現があるようで
development.rbconfig.action_controller.asset_host = 'myfullappurl.dev' config.action_mailer.asset_host = 'http://myfullappurl.dev'にしなければならない。
https://codeday.me/bug/20180720/199491.html
https://stackoverflow.com/questions/29887668/how-to-use-image-url-in-rails-to-show-images-in-production
- 投稿日:2019-01-27T16:33:24+09:00
jekyllでgithub pagesをささっと作る
前々から *.github.io を作りたいなと思っていて積み残しもくもく会で作ったのでそのメモ
まずは、 jekyll のページからインストール等々はしておいてください。
github側でリポジトリをつくる
まずはURLを決めるため、github側でリポジトリを作ります。
今回はテスト用に、abcという名前で作りました。設定を変更し、Github Pagesを有効にします。
作ったときは普通のリポジトリなので、GithubPagesとして扱うように設定を変更します。ここはdocsフォルダーにするとかはお好みで。(このメモ上ではmasterブランチで設定します)
ローカル側で jekyll で new します
$ jekyll new abc Running bundle install in /Users/***/work/github/new-github-page... Bundler: Fetching gem metadata from https://rubygems.org/........... Bundler: Fetching gem metadata from https://rubygems.org/. Bundler: Resolving dependencies... Bundler: Using public_suffix 3.0.3 Bundler: Using addressable 2.6.0 Bundler: Using bundler 2.0.1 Bundler: Using colorator 1.1.0 Bundler: Using concurrent-ruby 1.1.4 Bundler: Using eventmachine 1.2.7 Bundler: Using http_parser.rb 0.6.0 Bundler: Using em-websocket 0.5.1 Bundler: Using ffi 1.10.0 Bundler: Using forwardable-extended 2.6.0 Bundler: Using i18n 0.9.5 Bundler: Using rb-fsevent 0.10.3 Bundler: Using rb-inotify 0.10.0 Bundler: Using sass-listen 4.0.0 Bundler: Using sass 3.7.3 Bundler: Using jekyll-sass-converter 1.5.2 Bundler: Using ruby_dep 1.5.0 Bundler: Using listen 3.1.5 Bundler: Using jekyll-watch 2.1.2 Bundler: Using kramdown 1.17.0 Bundler: Using liquid 4.0.1 Bundler: Using mercenary 0.3.6 Bundler: Using pathutil 0.16.2 Bundler: Using rouge 3.3.0 Bundler: Using safe_yaml 1.0.4 Bundler: Using jekyll 3.8.5 Bundler: Using jekyll-feed 0.11.0 Bundler: Using jekyll-seo-tag 2.5.0 Bundler: Using minima 2.5.0 Bundler: Bundle complete! 4 Gemfile dependencies, 29 gems now installed. Bundler: Use `bundle info [gemname]` to see where a bundled gem is installed.The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`. Bundler: Following files may not be writable, so sudo is needed: Bundler: /Library/Ruby/Gems/2.3.0 Bundler: /Library/Ruby/Gems/2.3.0/build_info Bundler: /Library/Ruby/Gems/2.3.0/cache Bundler: /Library/Ruby/Gems/2.3.0/doc Bundler: /Library/Ruby/Gems/2.3.0/extensions Bundler: /Library/Ruby/Gems/2.3.0/gems Bundler: /Library/Ruby/Gems/2.3.0/specifications New jekyll site installed in /Users/***/work/github/abc.※ bundleの入れ方とか間違ってる気がする...。一旦気にしない...
できたフォルダ(abc)でgit initする
$ cd abc $ git init .Github側のファイルとつなぐ
git remoteを設定したりします。USERNAMEには自分のリポジトリのユーザ名を。
$ git remote add origin git@github.com:USERNAME/abc.git $ git fetch origin # この時点ではファイルはgithubのものとは一致しない $ ls 404.html Gemfile.lock _posts index.md Gemfile _config.yml about.md $ git checkout -b master origin/master # README.md が落ちてきます $ ls 404.html Gemfile.lock _config.yml about.md Gemfile README.md _posts index.mdこの内容をとりあえず初期ファイルとして送信します
$ git add . $ git commit $ git push origin masterあとは自動的にページが生成される
あがった
できあがり
- 投稿日:2019-01-27T13:10:09+09:00
Rails |onオプションを使ってバリデーションのタイミングを指定
はじめに
こんにちは、waruby510です。
今回、初めてQiitaへの記事を投稿させていただきます。
最近、色んな方の記事に助けてもらってばかりの状況なので、
備忘録&同じ初学者の力になれる内容 を簡単にですが、記事にしてみました。
内容は、タイトル通りですが、
validatesメソッドのonオプションを使って、バリデーションのタイミングを指定してみた
と言う感じです。
もちろん経験者にとっては、かなりイージーな内容になっています。初心者目線でこの記事を書いていますので、ご了承ください。
また、あまり奥深いところまでは書いていないので、今後更新していければと思います。<補足>
※初心者の立場で記事を書いています。最低限の下調べと実際に動くかどうかは確認していますが、もし誤った内容を記載していた場合は、ご指摘いただければと思います。※まだまだ文章を書くことに慣れていません。読みにくいかもしれませんが、どうかお付き合いください。
開発環境
Rails バージョン5.2.2
ruby バージョン2.5.3
※ログイン機能等に関しては、gemのdeviseは使っていません。アプリケーション詳細
- ユーザー登録・編集機能 ← 今回の記事の問題箇所
- 画像投稿、編集、削除機能 & いいね機能
簡単に言うと、インスタのようなアプリです。
発生した問題
ユーザー編集機能を追加した時にある問題に差し掛かりました。
それは、ユーザー編集画面にてパスワード欄を無くしたはずなのに、バリデーションがかかってしまう問題。
仕方なく、編集画面にもパスワード入力欄を設けて対応しましたが、
ユーザー目線で考えたら、使いにくいの一言。実際、userモデルに対しては、以下のようなバリデーションを追加していました。
user.rbclass User < ApplicationRecord #名前とメールアドレスに対するバリデーション validates :name, presence: true, length: {maximum: 30} validates :email, presence: true, length: {maximum: 255},format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i },uniqueness: true #略 #パスワードをハッシュ化 has_secure_password #パスワードに対するバリデーション validates :password, presence: true, length: {minimum: 6} #略 end考えられる原因
バリデーションのデフォルトでは、レコードの作成時、更新時。つまり保存が促されるときに実行されるように設定されている。つまり、編集時にパスワードがないとバリデーションがかかってしまいます。
冷静に考えてみると、ただ編集画面のパスワード欄の表示を消しているだけなので、バリデーションがかかるのも当たり前ですよね。笑考えれる対応策としては、バリデーションのかかるタイミングを指定できれば解決できそう。
実際にやったこと
Userモデルのpasswordカラムに対して、
on: :createを追加user.rb#on: :create を追加 validates :password, presence: true, length: {minimum: 6}, on: :create
ここでようやく
onオプションの登場です。
ここまで、長々と書いてきましたが、たったこれだけです......笑バリデーションのタイミングを特定して実行したい場合、
on: メソッド名とオプションを追加することで、そのアクションの時のみバリデーションを実行することができるみたいです。今回は、ユーザー登録時のみパスワードのバリデーションを起動させたいので、
on: :createとします。もしメソッド名を
on: updateにすれば、編集の更新時のみバリデーションがかかるという仕組みなのです。
個人的には結構使いそうな予感。今回は、RailsGuideの下記リンクを参考にしました。
参考リンク: RailsGuide
3.4 :on
:onオプションは、バリデーション実行のタイミングを指定します。ビルトインのバリデーションヘルパーは、デフォルトでは保存時に実行されます。これはレコードの作成時および更新時のどちらの場合にも行われます。バリデーションのタイミングを変更したい場合、on: :createを指定すればレコード新規作成時にのみ検証が行われ、on: :updateを指定すればレコードの更新時にのみ検証が行われます。class Person < ApplicationRecord # 値が重複していてもemailを更新できる validates :email, uniqueness: true, on: :create # 新規レコード作成時に、数字でない年齢表現を使用できる validates :age, numericality: true, on: :update # デフォルト (作成時と更新時のどちらの場合にもバリデーションを行なう) validates :name, presence: true end実際に、編集画面の動作を確認してみました。
見事にパスワードに対するバリデーションが回避されています。
少しはユーザー目線の機能に近づけた気がします。
今の自分の良いところは、初心者ということもあり、ユーザー目線に近い状態で開発ができるということ。この目線は今後も大事にしていきたいと思います。さいごに
結論が呆気なくて、すみませんでした。
自分自身もこれだけ?っていう感じでしたが、とてもいい勉強になりました。ただ、まだまだ便利な使い方が間違いなくあるはずなので、これから学習していく中で整理していき、また記事にできればと思います。
ここまで読んでくださった方、本当にありがとうございます。
今回は、初めてということもあり、Markdown記法に慣れるために色々な記法を使って書いてみました。そのため、無駄に記事が長くなってしまいましたが、個人的にはかなりいい練習ができたと思います。
この記事が誰かのお役に立てれれば本望です。
以上です。
- 投稿日:2019-01-27T00:54:03+09:00
【ruby】「DBリソース」と「AWS_S3上の関連するディレクトリ」を一緒に削除する実装(+Rspecテスト)
はじめに
S3上のディレクトリ削除処理とDBリソースと一緒に削除する実装について、色々勉強になったので記事にしておきます。
Tl;Dl;
- S3上のファイルの削除は以下の通り。
- 以下のように
batch_deleteを使うと指定のディレクトリ配下のファイル、ディレクトリを削除できるclass User < ActiveRecord def delete_all_s3_contents! s3 = Aws::S3::Resource.new(client: s3_client) objects = s3.bucket("bucket_name").objects(prefix: "users/#{self.id}") objects.batch_delete! end end
- DBリソースとS3上の削除処理を一緒に行う場合は、以下のようにtransactionを貼れば、どちらかの処理が失敗した時にDBリソース削除はロールバックされるので、データの整合性が担保できる。
class User < ActiveRecord def destroy_with_associating_resources ActiveRecord::Base.transaction do self.destroy! self.delete_all_s3_objects! end true rescue => e Rails.logger.error(e) false end endここから少し詳しく説明
説明したいことは、上述のコードだけで説明終了なのですが、もうちょっと詳しい説明を書きたいと思います。
今回説明する際の例として、 ユーザー削除機能 を考えます。そして、そのユーザー機能の1つとして写真投稿があり、その写真はS3上に保存しているとします。
その前提でユーザー削除機能を実装する場合、 DB内のユーザー情報に加えて、指定ユーザーに関連するS3上のファイルを一緒に削除することが必要 になります。
この記事は このユーザー削除+S3上のファイル削除を考える話 となります。
ユーザーとS3上のファイルアップロード先の関係
ユーザーが投稿した写真は、AWSのS3上の
/bucket_name/contents/users/:idにアップロードするとします。:idはユーザーのID(数字)です。そのため、例えばIDは1のユーザーを削除する場合、S3上の
/bucket_name/contents/users/1/パスのディレクトリをファイルを含めて削除することを意味します。実装
以上の説明を踏まえて、作成した実装が以下の通りです。
class User < ApplicationRecord def delete_all_s3_contents! s3 = Aws::S3::Resource.new(client: s3_client) objects = s3.bucket("bucket_name").objects(prefix: "contents/users/#{self.id}") objects.batch_delete! end def destroy_with_associating_resources ActiveRecord::Base.transaction do self.destroy! self.delete_all_s3_objects! end true rescue => e Rails.logger.error(e) false end private def s3_client @s3_client ||= Aws::S3::Client.new( access_key_id: "ACCESS_KEY_ID", secret_access_key: "SECRET_ACCESS_KEY", region: "ap-northeast-1", ) end endS3上の指定ディレクトリの削除
def delete_all_s3_contents! s3 = Aws::S3::Resource.new(client: s3_client) objects = s3.bucket("bucket_name").objects(prefix: "contents/users/#{self.id}") objects.batch_delete! end指定ディレクトリ削除の流れは、まず
s3.bucket("bucket_name").objects(prefix: "contents/users/#{self.id}")にて、バケット名bucket_nameのcontents/users/#{self.id}ディレクトリにある全てのオブジェクト一覧を取得します。そして、
batch_delete!を呼び出し、先ほど取得したオブジェクト一覧をパラメタとして、一括でS3に削除APIをリクエストします。
s3_clientはプライベートメソッドとして切り出しています。理由は、その方がテストのモックが簡単になるからです。private def s3_client @s3_client ||= Aws::S3::Client.new( access_key_id: "ACCESS_KEY_ID", secret_access_key: "SECRET_ACCESS_KEY", region: "ap-northeast-1", ) endまた、AWSのS3に対するAPIリクエストを行なった際に、失敗したら例外が吐かれる想定で実装しています。実際のコードで例外送出を行う箇所を見つけられなかったので正確ではないですが、以下のS3のSDKのスタブに関する記事をみる限り、失敗時は例外を吐くと認識しています(間違っていたら、ご指摘ください)
https://docs.aws.amazon.com/ja_jp/sdk-for-ruby/v3/developer-guide/stubbing.htmlユーザー削除処理
def destroy_with_associating_resources ActiveRecord::Base.transaction do self.destroy! self.delete_all_s3_objects! end true rescue => e Rails.logger.error(e) false endユーザー削除(
self.destroy!)とS3上のファイル削除(self.delete_all_s3_objects!)の2つに対して、transactionを貼ることで、どちらかが失敗して例外を早出したら、DBロールバックによってリクエスト実行前に戻ります。ここのポイントは、
self.delete_all_s3_objects!を後にすることですね。処理を逆にしてしまうと、S3上のファイル削除後に行われるユーザー削除が失敗した場合に、S3上のファイル削除だけ行われてしまうのでデータの整合性が保つことができなくなります。テスト
次に、S3削除処理(
delete_all_s3_contents!)に対するテストについて説明します。全体の流れは以下の通りです。describe :delete_all_s3_contents! do subject { user.delete_all_s3_contents! } let(:user) { User.create(name: "hoge") } let(:s3_contents) { [ { key: "contents/users/1/IMAGE.png" }, { key: "contents/users/1" }, ] } let(:client) { Aws::S3::Client.new(stub_responses: true) } before do client.stub_responses(:list_objects, contents: s3_contents) client.stub_responses(:delete_objects) allow(user).to receive(:s3_client).and_return(client) end it "指定のパラメタでS3のAPIへリクエストが行われていること" do expect(client).to receive(:list_objects). with({ bucket: "bucket_name", prefix: "contents/#{user.id}", }).and_call_original expect(client).to receive(:delete_objects). with({ bucket: "bucket_name", delete: { objects: s3_contents }, }) subject end end前準備
まず
beforeでの前準備の説明です。まず、APIスタブ用のAWSのS3 clientインスタンスを作成しています。S3Clientはオプションを指定するとスタブができるようになります。
let(:client) { Aws::S3::Client.new(stub_responses: true) }次に、clientから実行するS3のAPIリクエストのスタブを行います。
before do client.stub_responses(:list_objects, contents: s3_contents) client.stub_responses(:delete_objects) allow(user).to receive(:s3_client).and_return(client) endS3のsdkには
stub_responsesというスタブ用のメソッドが用意されています(リファレンス)。今回は
batch_delete!で指定のオブジェクトの削除を行うのですが、実際にS3側へのリクエストはlist_objectsとdelete_objectsの2つが行われます。なので、その2つをスタブしてあげれば良いです。最後に、
userインスタンスのプライベートメソッドs3_clientが呼ばれた際に、モック用のS3 clientインスタンスclientを返すよう定義します。これでS3へのリクエストをスタブすることができました。検証
実際の検証部分は以下の通りです。
it "指定のパラメタでS3のAPIへリクエストが行われていること" do expect(client).to receive(:list_objects). with({ bucket: "bucket_name", prefix: "contents/users/#{user.id}", }).and_call_original expect(client).to receive(:delete_objects). with({ bucket: "bucket_name", delete: { objects: s3_contents }, }) subject end
expectのwithと使うことでパラメタの検証ができるので、「指定のパラメタでS3へリクエストを送っているか?」を検証しています。あと、今回初めて知った点で
and_call_originalです。通常expectで対象となったメソッドはデフォルトではレスポンスを返さなくなります。ただ、今回はlist_objectsのレスポンスを使ってdelete_objectsを実行するのでレスポンスがないと以後の検証ができません。なので、and_call_originalを呼ぶことで、ちゃんとレスポンスを返す指定をしています。おわりに
S3の削除の実装、テストを書きながら、色々勉強になった点をまとめました。今後もテストが書きやすく、わかりやすいコードを書けるように日々精進していきたいです。
参考文献










