20190127のRubyに関する記事は6件です。

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センター"

スクリーンショット 2019-01-27 22.23.02.png
欲しい結果は取得できていますが、検索ワードが"日本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"

スクリーンショット 2019-01-27 22.22.31.png
上記の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とのことなので、今後は使用しないほうがよいかもしれません。

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

[Ruby] irbで、矢印キーを押すと`^[[D`などと出てしまう現象の解決法

環境

  • macOS 10.13.6
  • Ruby 2.5.3

現象

irbで矢印キーを押すと、次のような記号が現れてしまうようになってしまいました。
たとえば、文字を修正しようと←キーを押すと…
strange_symbol.png
こうなってしまいます。

原因

調べてみると、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とリネームすることで、無事に解決しました。
libreadline7.png

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

RailsのActionMailerで画像のURLを絶対パスで表示させる

ActionMailerで送信したメールに絶対パスで画像を表示させたい中でハマった。

Mail内で以下のように表示させるために

index.html
 <img src="http://myfullappurl.dev/assets/myimage.png">

development.rbにて以下のようにメーラーを設定

development.rb
config.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.rb
URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}

有効なActionMailer URIを定義する正規表現があるようで

development.rb
config.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

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

jekyllでgithub pagesをささっと作る

前々から *.github.io を作りたいなと思っていて積み残しもくもく会で作ったのでそのメモ

まずは、 jekyll のページからインストール等々はしておいてください。

github側でリポジトリをつくる

まずはURLを決めるため、github側でリポジトリを作ります。
今回はテスト用に、 abc という名前で作りました。

設定を変更し、Github Pagesを有効にします。

image.png

作ったときは普通のリポジトリなので、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

あとは自動的にページが生成される

image.png

あがった

image.png

できあがり

https://tetsunosuke.github.io/abc/

image.png

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

Rails |onオプションを使ってバリデーションのタイミングを指定

はじめに

こんにちは、waruby510です。

今回、初めてQiitaへの記事を投稿させていただきます。

最近、色んな方の記事に助けてもらってばかりの状況なので、
備忘録&同じ初学者の力になれる内容 を簡単にですが、記事にしてみました。


内容は、タイトル通りですが、

validatesメソッドのonオプションを使って、バリデーションのタイミングを指定してみた

と言う感じです。

もちろん経験者にとっては、かなりイージーな内容になっています。初心者目線でこの記事を書いていますので、ご了承ください。
また、あまり奥深いところまでは書いていないので、今後更新していければと思います。

<補足>
※初心者の立場で記事を書いています。最低限の下調べと実際に動くかどうかは確認していますが、もし誤った内容を記載していた場合は、ご指摘いただければと思います。

※まだまだ文章を書くことに慣れていません。読みにくいかもしれませんが、どうかお付き合いください。

開発環境

Rails バージョン5.2.2
ruby バージョン2.5.3
※ログイン機能等に関しては、gemのdeviseは使っていません。

アプリケーション詳細

  • ユーザー登録・編集機能 ← 今回の記事の問題箇所
  • 画像投稿、編集、削除機能 & いいね機能

簡単に言うと、インスタのようなアプリです。

発生した問題

ユーザー編集機能を追加した時にある問題に差し掛かりました。
それは、ユーザー編集画面にてパスワード欄を無くしたはずなのに、バリデーションがかかってしまう問題。

参考画像1: ユーザー編集画面にてバリデーション起動
スクリーンショット 2019-01-27 11.32.43.png

仕方なく、編集画面にもパスワード入力欄を設けて対応しましたが、
ユーザー目線で考えたら、使いにくいの一言。

実際、userモデルに対しては、以下のようなバリデーションを追加していました。

user.rb
class 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

実際に、編集画面の動作を確認してみました。

参考画像2: ユーザー編集画面にてバリデーション回避
スクリーンショット 2019-01-27 12.35.13.png

見事にパスワードに対するバリデーションが回避されています。
少しはユーザー目線の機能に近づけた気がします。
今の自分の良いところは、初心者ということもあり、ユーザー目線に近い状態で開発ができるということ。この目線は今後も大事にしていきたいと思います。

さいごに

結論が呆気なくて、すみませんでした。
自分自身もこれだけ?っていう感じでしたが、とてもいい勉強になりました。

ただ、まだまだ便利な使い方が間違いなくあるはずなので、これから学習していく中で整理していき、また記事にできればと思います。

ここまで読んでくださった方、本当にありがとうございます。

今回は、初めてということもあり、Markdown記法に慣れるために色々な記法を使って書いてみました。そのため、無駄に記事が長くなってしまいましたが、個人的にはかなりいい練習ができたと思います。

この記事が誰かのお役に立てれれば本望です。

以上です。

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

【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
end

S3上の指定ディレクトリの削除

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_namecontents/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)
end

S3のsdkにはstub_responsesというスタブ用のメソッドが用意されています(リファレンス)。

今回はbatch_delete!で指定のオブジェクトの削除を行うのですが、実際にS3側へのリクエストは list_objectsdelete_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

expectwithと使うことでパラメタの検証ができるので、「指定のパラメタでS3へリクエストを送っているか?」を検証しています。

あと、今回初めて知った点で and_call_original です。通常expectで対象となったメソッドはデフォルトではレスポンスを返さなくなります。ただ、今回はlist_objectsのレスポンスを使ってdelete_objects を実行するのでレスポンスがないと以後の検証ができません。なので、and_call_originalを呼ぶことで、ちゃんとレスポンスを返す指定をしています。

おわりに

S3の削除の実装、テストを書きながら、色々勉強になった点をまとめました。今後もテストが書きやすく、わかりやすいコードを書けるように日々精進していきたいです。

参考文献

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