20200112のRubyに関する記事は21件です。

Rspec, Factory_bot, Fakerを用いたモデル単体テスト

はじめに

  • Rspec, Factory_bot, Fakerを用いたテストについてメモを残したいと思います。
  • 今回はuserモデルでテストします。
  • 開発環境
  • ruby 2.5.1
  • rails 5.2.4.1

gemの導入

Gemfile
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end 
group :test do
  gem 'faker'
end

gemを追加したら、bundle installする。

Rspecの設定

terminal
$ rails g rspec:install
# Rspecの設定ファイルが作成される。
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb
  • spec/spec_helper.rb ・・・Rails無しでRspecを利用するときに使う。
  • spec/rails_helper.rb ・・・RailsでRspecを利用するときに使う。
.rspec
# 以下を追加することで出力を見やすくできる。
--format documentation

Factory_botの設定

  1. specディレクトリ下に「factories」ディレクトリを作成。
  2. その中に「users.rb」ファイルを作成。
  3. spec/factories/users.rbを以下のように編集する。
spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name                  {"abe"}
    sequence(:email)      {Faker::Internet.email}
    phone_number          {Faker::PhoneNumber.phone_number}
  end
end
  • fakerを使用し、ダミーデータを作成。他にもいろいろなfakerがあります。

Factory_botの記述の省略

spec/rails_helper.rb
# 上記省略
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods #追加する
# 下記省略
end

「config.include FactoryBot::Syntax::Methods」 を追加することで以下のように省略できる。

# 追加前
user = FactoryBot.build(:user)
# 追加後
user = build(:user)

モデルテストの記述

  • app/models/user.rb にバリデーションを記述します。
app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 6 }
  validates :email, presence: true, uniqueness: true
  validates :phone_number, presence: true
end
  • spec/models/users_spec.rbにバリデーションが正常に機能しているか確かめるコードを記述します。
spec/models/users_spec.rb
require 'rails_helper'

describe User do
  describe '#create' do
    it "name, email, phone_numberがあれば有効" do
      user = build(:user)
      expect(user).to be_valid
    end

    it "nameがないと無効" do
      user = build(:user, nickname: nil)
      user.valid?
      expect(user.errors[:nickname]).to include("can't be blank")
    end

    it "emailがないと無効" do
      user = build(:user, email: nil)
      user.valid?
      expect(user.errors[:email]).to include("can't be blank")
    end

    it "phone_numberがないと無効" do
      user = build(:user, phone_number: nil)
      user.valid?
      expect(user.errors[:phone_number]).to include("can't be blank")
    end

    it "emailが重複していたら無効" do
      user = create(:user)
      another_user = build(:user, email: user.email)
      another_user.valid?
      expect(another_user.errors[:email]).to include("has already been taken")
    end

    it "nameが7以上だったら無効" do
      user = build(:user, nickname: "a" * 7)
      user.valid?
      expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")
    end

    it "nameが6以下だったら有効" do
      user = build(:user, nickname: "a" * 6)
      expect(user).to be_valid
    end
  end
end

ちなみに、lengthのバリデーションについて忘れないようにメモ。

  validates :name, length: { maximum: 10 }  # 10以下、 最高で10文字まで
  validates :name, length: { minimum: 6 }  # 6以上、 最低でも6文字必要
  validates :name, length: { in: 6..10 }  # 6文字以上10以下
  validates :name, length: { is: 6 }  # 6文字のみ

Rspecを実行する

terminal
  # 特定のファイルを実行
  $ bundle exec rspec spec/models/users_spec.rb
  # すべてのファイルを実行
  $ bundle exec rspec

まとめ

間違っていることやアドバイスなどご指摘いただければ助かります。
よろしくお願いいたします。

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

【Rails入門】エラーと仲良くなれるかもしれない開発手法【読み物】

どんもー、@snskOgataです。

今回はみんな大好き、エラーとの付き合い方を書いていこうかなと思います。
まあ、ちょっとした読み物として楽しんでもらえたらと思います。
 
 
対象読者は以下の2つに当てはまる人です。
・エラーという存在が憎くて憎くてしょうがない
・Railsアプリを読み物を見ながら1, 2度作ってみたけど、実際に自分で何も見ずに作るとなったらどうやっていけばいいかわからない

 
エラーが出るとどうしても怒られているような感覚になってしまいますが、
そうじゃなくて本当は、エラーは正解への道筋を示してくれる相棒のようなものだということを感じてくれると嬉しいです。

 
実際やっていくこととしては、
エラーを吐かせながら、それに従ってアプリケーションを作っていくということをしていきます。
これによりひとつずつ着実に作業を進めることができます。
アプリの内容は、つぶやきを作成しその一覧を表示する簡単なアプリケーションです。
途中で大体もう流れがわかったら最後まで飛ばしてくれても大丈夫です(笑)

1. Setup

まずはアプリケーションをビルドします。
Railsバージョンは5.2.3を使用します

$ rails _5.2.3_ new dev-with-error
$ cd dev-with-error
$ bundle
$ rails db:migrate

やってることはRailsアプリケーションを立ち上げ、フォルダを移動して、gemファイルのインストール、DBの作成です。
ここまでやるとローカルサーバを立ち上げることができます。

$ rails s

Webブラウザで「localhost:3000」にアクセスすればお馴染みの画面が出てきます。
スクリーンショット 2020-01-12 18.02.46.png

2.Tweet作成画面を作る

次にTweetの新規作成画面を作っていきたいと思います。
ここで通常であれば、ルーティングをして、コントローラとモデルを作って、適切なビューを作って...とやっていくと思いますが、
今回は冒頭でも書いたようにまずはエラーを吐かせてそれに従って作っていくというふうに作って行きます。

実際にやることとしては、Tweetの新規作成画面ということで「localhost:3000/tweets/new」というアドレスにアクセスをします
スクリーンショット 2020-01-12 18.10.19.png
まだアプリケーション本体に何も書き込んでいないわけなので当然エラーを吐かれます。
エラーの内容を見てみると、ルーティングがされていないよ!とのエラーが出ているので、実際にルーティング部分を書き加えます。

config/routes.rb
Rails.application.routes.draw do
  resources :tweets, only: :new
end

もう一度先ほどのURLにアクセスすると次はこのようなエラーが出ます。スクリーンショット 2020-01-12 18.16.37.png
今度はTweetsControllerがないのでそれを教えてくれます。
エラーに従ってコントローラを作成しましょう。

$ rails g controller tweets

スクリーンショット 2020-01-12 18.18.44.png

newアクションが見つからないということでTweetsControllerに追記します。

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  def new
  end
end

保存をし再びページをリロードすると、
スクリーンショット 2020-01-12 18.23.05.png
随分長いエラー文となっていますが、冒頭の「TweetsController#new is missing a template」から、newアクションに対応するビューのテンプレートがないというのがエラーの原因というのがわかります。
なので、Tweets#newに対応するように、「views/tweets/new.html.erb」を作成します。

app/views/tweets/new.html.erb
Hello

「localhost:3000/tweets/new」にアクセスすると、
スクリーンショット 2020-01-12 18.29.09.png
おめでとう!これで新規作成画面の表示に成功しました!

さて、新規投稿するためにインスタンス変数@​tweetを用意しましょう。

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  def new
    @tweet = Tweet.new
  end
end

ここで再びページリロードをすると
スクリーンショット 2020-01-12 18.34.52.png
Tweetというものがわからない!と言われるので、Tweetモデルを作成してあげます

$ rails g model tweet content:string
...

モデルが生成されましたが
スクリーンショット 2020-01-12 18.38.12.png
問題を解決するには「rails db:migrate」して、と書かれています。
どうやらマイグレーションファイルをDBに適用していないのが問題みたいです。

$ rails db:migrate

これで再びエラー無しの状態になりました!
次に、実際にビューに今回用意したインスタンス変数を使って、フォームを作成してみます。

app/views/tweets/new.html.erb
<%= form_with model: @tweet do |form| %>
  <%= form.text_field :content %>
  <%= form.submit %>
<% end %>

ここでのエラーはわかりづらいですが、form_withによってtweets_pathにポストがなされるのですが、そのメソッド(:create)が宣言されていないためにのエラーとなります。
スクリーンショット 2020-01-12 18.45.24.png
ルーティングで「:create」を追加してあげましょう。

config/routes.rb
Rails.application.routes.draw do
  resources :tweets, only: [:new, :create]
end

再びエラー無しに!
スクリーンショット 2020-01-12 18.51.00.png
ただしcreateアクションを実際に実装していないため、フォームを入力しても何も変わりません。
なのでコントローラにcreateを追記してあげましょう。
ついでに新規投稿画面に、投稿された一覧を表示できるようにしましょう。

app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  def new
    @tweet = Tweet.new
    @tweets = Tweet.all #追記:全投稿取得→表示に使用
  end

  def create
    Tweet.create(tweet_params) # DBに保存
    redirect_to new_tweet_path # 新規投稿画面に再び戻る
  end

  private
    # ストロングパラメータ
    def tweet_params
      params.require(:tweet).permit(:content)
    end
end
app/views/tweets/new.html.erb
<% # local: trueにすることで、送信後ページが更新される %> 
<%= form_with model: @tweet, local: true do |form| #local %>
  <%= form.text_field :content %>
  <%= form.submit %>
<% end %>

<p>Tweets</p>
<% # Tweetの一覧表示 %> 
<%- @tweets.each do |tweet| %>
  <%= "#{tweet.content} <#{tweet.created_at}>" %>
  <br/>
<% end %>

無事、簡単なつぶやき投稿アプリができました!
スクリーンショット 2020-01-12 20.44.59.png

同様にして編集ページや一覧表示ページ、あるいは新しいコントローラやモデルを作成していけば、ある程度のものであればエラーに従っていくだけで作れてしまうと思います!

3. 一気にRMVCを作ってしまう弊害

この手法だとエラーに従ってひとつずつパーツを揃えていくため、今コーディングしている部分が正しくなければ先に進むことができません。
すなわち、エラー部分が限定されて間違っている可能性がある部分というのがすごく限定的になります。

Railsを始めたての人は、MVCを一度に実装してしまったために、エラーが発生したときに何処で起きているのかがわからなくなってしまう、という問題に遭遇してる人が多い様な気がします。
なので、こういうように着実に進められる方法もあるよ、エラーは怒ってるんじゃなくて導いてくれてるんだよ、ってのを少しでも実感してくれたら幸いです。

4.テスト駆動開発

こういう開発手法を思いついたきっかけがテスト駆動開発(TDD)です。
この手法は、今回エラーを起こして進めて行ったような感じで、まずは通らないテストを書いて、それに通るように実装をしていくという手法です。
実際の現場でも使われている手法で1、一度は経験しておいて損はないかなと思います。
読みながらならある程度簡単なアプリなら作り方が理解できる方なら、Rails Tutorialという本が無料で公開されているので挑戦してみると良いかもしれません。(※Rails Tutorialはminitestというデフォルトのテストを用いているのでRSpecを使ってる方は違いには注意!)
Rails Tutorial:https://railstutorial.jp/chapters/beginning?version=5.1#cha-beginning

加えてここではgem(deviseなど)を使わずにユーザ管理を実装するのですが、Deviseの作者も一度は自分の手でUserモデルを作成することを推奨しているので、その点でもオススメです。

追記:ついでにHerokuで簡単にデプロイする方法とか、自作ヘルパーメソッドの実装とかもすごくタメになります!


  1. 実際はコストの面との相談で使われなかったりする? テスト自体は非常に重要なことには変わりないですが! 

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

Ruby on Rails APIモードのCRUD実装 【初学者のReact✗Railsアプリ開発 第5回】

やったこと

  • Ruby on RailsのAPIモードでCRUDを実装する(ただし、更新(U)はなし)

前回の記事

Reactのreduxを用いたログイン処理周りの実装【初学者のReact✗Railsアプリ開発第4回】

参考にさせていただいた記事

https://qiita.com/k-penguin-sato/items/adba7a1a1ecc3582a9c9

実装手順

モデルとコントローラーの作成

$ docker-compose run api rails g model post content:string
$ docker-compose run api rails g controller api/v1/posts

生成されたマイグレーションファイルを編集します。

db/migrate/XXX_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t|
      t.string :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :posts, [:user_id, :created_at]
  end
end
$ docker-compose run api rake db:migrate 

route.rb

route.rb
Rails.application.routes.draw do
 namespace :api, defaults: { format: :json } do
    namespace :v1 do
      ##省略##
      resources :posts
    end
 end
end

  • resourcesで、GET, POSTなど複数のルーティングを一気に設定できる。resourceとの違いに注意。

posts_controller

posts_controller
module Api
  module V1
    class PostsController < ApplicationController
      before_action :set_post, only: [:index, :show, :update, :destroy]
      before_action :authenticate_api_v1_user!

      def index
        posts = Post.all
        render json: { status: 'SUCCESS', message: 'Loaded posts', data: posts}
      end

      def show
        @user = @post.user
        json_data = {
          'post': @post,
          'user': {
            'name': @user.name,
            'nickname': @user.nickname,
            'image': @user.image
          }
        }
        render json: { status: 'SUCCESS', message: 'Loaded the post', data: json_data}
      end

      def create
        post = Post.new(post_params)
        if post.save
          render json: { status: 'SUCCESS', data: post}
        else
          render json: { status: 'ERROR', data: post.errors }
        end
      end

      def destroy
        @post.destroy
        render json: { status: 'SUCCESS', message: 'Delete the post', data: @post}
      end

      def update
      end

      private

      def set_post
        @post = Post.find(params[:id])
      end

      def post_params
        params.require(:post).permit(:content, :user_id)
      end

    end
  end
end  

models/post.rb

post.rb
class Post < ApplicationRecord
  belongs_to :user
end

Postmanを用いてAPIの動作確認をする

create

chromeのデベロッパーツール->Application->Local Storageからauth_tokenとかをコピーして、
スクリーンショット 2020-01-12 21.43.04.png
Postmanに貼り付ける。
スクリーンショット 2020-01-12 21.42.44.png
そしてlocalhost:3000/api/v1/postsにPOSTすると
スクリーンショット 2020-01-12 21.41.24.png
postが作成されたことが確認できます。

index

localhost:3000/api/v1/postsにGETすると
スクリーンショット 2020-01-12 21.45.34.png

show

localhost:3000/api/v1/posts/1にGETすると、idが1のpostが返されます。
スクリーンショット 2020-01-12 21.45.52.png

destroy

localhost:3000/api/v1/posts/1にDELETEすると、idが1のpostが消えます。
スクリーンショット 2020-01-12 21.46.06.png
indexで確認すると、消えています。
スクリーンショット 2020-01-12 21.46.17.png

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

【Ruby】ある整数の約数(要素の数、和)を計算する

興味本位で書いてみた結果の、備忘録用です。
(メソッドの定義文については、幸運にもscivola様からアドバイスを頂きました、1行でのリファクタリングが可能です。是非ご参照ください。)

divisor.rb
def divisor(num)
  result = []
  i = 1
  while i <= num do
    remainder = num % i
    if remainder == 0
      result << i
    end 
    i += 1
  end
  return result
end

puts "約数を算出したい整数を入力してください"
num = gets.to_i
r = divisor(num)
puts r
puts "約数の数は#{r.length}です"
puts "約数の合計は#{r.sum}です"

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

【Ruby】ある整数の約数(要素の数、和)を求める

興味本位で書いてみた結果の、備忘録用です。
(メソッドの定義文については、幸運にもscivola様からアドバイスを頂きました。1行でのリファクタリングが可能です。是非コメント欄をご参照ください。)

divisor.rb
def divisor(num)
  result = []
  i = 1
  while i <= num do
    remainder = num % i
    if remainder == 0
      result << i
    end 
    i += 1
  end
  return result
end

puts "約数を算出したい整数を入力してください"
num = gets.to_i
r = divisor(num)
puts r
puts "約数の数は#{r.length}です"
puts "約数の合計は#{r.sum}です"

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

Railsチュートリアルメモ - 第7章

メモの目次記事はこちら

Railsチュートリアル第7章へのリンク

第7章 ユーザー登録

7.1 ユーザーを表示する

ポイント

デバッグ

  • <%= debug(params) if Rails.env.development? %>で画面にデバッグ情報を表示できる。
    • if Rails.env.development?で開発環境のみに表示するよう制限している
  • コントローラー内にdebuggerを記載して画面を表示すると、rails serverを起動しているコンソールがコマンド待受け状態になり、debuggerが呼び出された状態のままでrails consoleの操作を実行できる

Sassのミックスイン

@mixinで定義した内容を@includeで呼び出せる

@mixin box_sizing {
  -moz-box-sizing:    border-box;
  -webkit-box-sizing: border-box;
  box-sizing:         border-box;
}
/* miscellaneous */
.debug_dump {
  clear: both;
  float: left;
  width: 100%;
  margin-top: 45px;
  @include box_sizing;
}

ルーティング

config/routes.rbにresourceを追加すると、名前付きルートが使用可能になり、RESTに従ったいくつかのURLが使用可能になる

config/routes.rb
Rails.application.routes.draw do

  resources :users
end

Gravatar

RailsというよりGravatarの使用方法の説明なので割愛

7.2 ユーザー登録フォーム

ポイント

form_forによるフォームの作成

.html.erb内でform_forを使用すると、フォームを生成してくれる

.html.erb

<%= form_for(@user) do |f| %>
...
<%= f.label :name %>
<%= f.text_field :name %>
...
<% end %>


html

<form action="/users" class="new_user" id="new_user" method="post">
...
<label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />
...
</form>
  • また、emailであれば、モバイル端末から入力フォームをタップすると、メールアドレスに最適化された特別なキーボードが表示される。passwordであれば、文字が隠蔽されて表示されるようにHTMLを生成してくれる。
  • CSRF対策のトークンの生成も行ってくれる

paramsハッシュでの値の受け渡し

フォームで指定した値はparamsというハッシュに保存されてコントローラーに渡されるルールになっている(チュートリアルでの説明があまりにさらっとしすぎていて、paramsがどこで定義されているのかわからず少し混乱した)

7.3 ユーザー登録失敗

ポイント

Strong Parameters

マスアサインメント:
DB登録・更新で複数のカラムを一括で指定して登録すること。メソッドにハッシュを渡して更新カラムと値を指定する。
意図的に重要なカラムの値を書き換えられる危険性があるため、Strong Parametersというテクニックを使ってController層で対策することが推奨されている(かつてはModel層で対策していたらしい)

params.require(:user).permit(:name, :email, :password, :password_confirmation)

privateキーワード

クラス内でprivateと記載された後に記載された要素はprivate属性となる(外部クラスからは見えない)

エラー表示

  • Rails全般の慣習として、複数のビューで使われるパーシャルは専用のディレクトリ「shared」によく置かれる
  • pluralizeメソッドによって、英語の単数系と複数形を良い感じに処理して表示してくれる
    • e.g. 0 errors, 1 error, 2 errors
  • Railsは、無効な内容の送信によって元のページに戻されると、CSSクラスfield_with_errorsを持ったdivタグでエラー箇所を自動的に囲んでくれる
  • (エラー表示とは関係ないが、)class form-controlを追加すると、Bootstrapでフォームがきれいになる

Sassの@extend関数

@extendで特定の位置に属性を追加できる

.field_with_errors {
  @extend .has-error;
  .form-control {
    color: $state-danger-text;
  }
}

統合テスト

  • rails generate integration_test users_signupで統合テストの作成

assert_no_difference, assert_difference

以下のように記載することで、ブロック内の実行前後で引き数が変化していないこと、変化していることを検証できる

assert_no_difference 'User.count' do

end
assert_difference 'User.count' do

end

assert_template

以下のように記載することで、意図どおりページが再描画されているかどうかを検証できる(エラーメッセージの表示によるDOMの差異は無視される?)

    assert_template 'users/new'

7.4 ユーザー登録成功

ポイント

redirect_to

コントローラーに以下のように記載すると、自動的に名前付きルート付きで解釈してくれる

    if @user.save
      redirect_to @user
    else
      render 'new'
    end

    if @user.save
      redirect_to user_url(@user)
    else
      render 'new'
    end

flash変数

flash変数に代入したメッセージは、リダイレクトした直後のページでのみ表示できる(二回目は表示されない)

シンボル => 文字列の自動変換

Railsではシンボルをテンプレート(.html.erb)内に表示しようとすると、文字列に自動変換する
e.g. :success => "success"

7.5 プロのデプロイ

ポイント

WebサーバPumaの導入

  • config/environments/production.rbconfig.force_ssl = trueをコメントインすると、本番環境でのSSL化が有効になる
  • Rails4.2まではconfig/puma.rbの手動作成が必要だったが、Rails5以降はデフォルトで作成済み
  • Procfileをプロジェクトルートに作成してherokuにgit pushすれば反映される

./Procfile

web: bundle exec puma -C config/puma.rb
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】Rubyで超簡単ドリンク注文アプリケーションを作る

メソッドを使用して、ドリンク注文アプリケーションを作ってみる

条件
 ・実行時、どのようなドリンクがあるかを表示させる
 ・ドリンクには番号をつける
 ・選ばれた番号と同じドリンク名を表示させる

擬似コードを書く

def otya
 お茶が選ばれたときのメソッド 
end

def coffee
  コーヒーが選ばれた時のメソッド
end

def beer
  ビールが選べれた時のメソッド
end

def saida
  サイダーが選べれた時のメソッド
end


  puts "何を飲みたいですか?"
    お茶、コーヒー、ビール、サイダーと、それぞれのドリンクメニューを表示させる

   1の時お茶
   2の時コーヒー
   3の時ビール
   4の時サイダー
   それ以外は
   "無効な入力値です"と表示させる
    end

・それぞれのドリンクが選ばれた際のメソッドを用意しておく(今回はただ、文字を表示させるだけ)
・ドリンクメニューはeach.with_indexを使用し、メニューの左に1から番号をつける
・case whenを使用し、条件分岐を書いて、それぞれのメソッドが動くように書く

実際にコーディングしていく

def otya
  puts "あなたが選んだのはお茶"
end

def coffee
  puts "あなたが選んだのはコーヒー"
end

def beer
  puts "あなたが選んだのはビール"
end

def saida
  puts "あなたが選んだのは三ツ矢サイダー"
end


  puts "何を飲みたいですか?"
    drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"]
    drink_menu.each.with_index(1) do |drink_name, number|
      puts "#{number}:#{drink_name}"
    end

  case gets.to_i
    when 1
      otya
    when 2
      coffee
    when 3
      beer
    when 4
      saida
    else
      puts "無効な入力値です"
    end

動作確認

何を飲みたいですか?
1:お茶
2:コーヒー
3:ビール
4:三ツ矢サイダー
1
あなたが選んだのはお茶

メソッド名は本当はよくないんですが、適当です。。。
今回、メソッドを使ったアプリケーションを作ってみたかった。。。

まだまだ未熟ですが、プログラム考えてる時ってワクワクして楽しいですよね。
次はもうちょっと複雑な処理もやってみようと思います。

最後までみていただきありがとうございます。

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

【初心者向け】超簡単ドリンク注文アプリケーション

メソッドを使用して、ドリンク注文アプリケーションを作ってみる

条件
 ・実行時、どのようなドリンクがあるかを表示させる
 ・ドリンクには番号をつける
 ・選ばれた番号と同じドリンク名を表示させる

擬似コードを書く

def otya
 お茶が選ばれたときのメソッド 
end

def coffee
  コーヒーが選ばれた時のメソッド
end

def beer
  ビールが選べれた時のメソッド
end

def saida
  サイダーが選べれた時のメソッド
end


  puts "何を飲みたいですか?"
    お茶、コーヒー、ビール、サイダーと、それぞれのドリンクメニューを表示させる

   1の時お茶
   2の時コーヒー
   3の時ビール
   4の時サイダー
   それ以外は
   "無効な入力値です"と表示させる
    end

・それぞれのドリンクが選ばれた際のメソッドを用意しておく(今回はただ、文字を表示させるだけ)
・ドリンクメニューはeach.with_indexを使用し、メニューの左に1から番号をつける
・case whenを使用し、条件分岐を書いて、それぞれのメソッドが動くように書く

実際にコーディングしていく

def otya
  puts "あなたが選んだのはお茶"
end

def coffee
  puts "あなたが選んだのはコーヒー"
end

def beer
  puts "あなたが選んだのはビール"
end

def saida
  puts "あなたが選んだのは三ツ矢サイダー"
end


  puts "何を飲みたいですか?"
    drink_menu = ["お茶","コーヒー","ビール","三ツ矢サイダー"]
    drink_menu.each.with_index(1) do |drink_name, number|
      puts "#{number}:#{drink_name}"
    end

  case gets.to_i
    when 1
      otya
    when 2
      coffee
    when 3
      beer
    when 4
      saida
    else
      puts "無効な入力値です"
    end

動作確認

何を飲みたいですか?
1:お茶
2:コーヒー
3:ビール
4:三ツ矢サイダー
1
あなたが選んだのはお茶

メソッド名は本当はよくないんですが、適当です。。。
今回、メソッドを使ったアプリケーションを作ってみたかった。。。

まだまだ未熟ですが、プログラム考えてる時ってワクワクして楽しいですよね。
次はもうちょっと複雑な処理もやってみようと思います。

最後までみていただきありがとうございます。

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

RailsのActiveStorageで画像が表示されずTypeError(no implicit conversion of nil into String)がでるようになった

状況

ローカルで開発していたアプリをDockerで構築し直していたらActiveStorageを使用して表示していた画像が表示されなくなった
ログを見ると以下のようにTypeError (no implicit conversion of nil into String)が発生してる

Started GET "/rails/active_storage
~ (省略)~
Processing by ActiveStorage::DiskController#show as PNG
Parameters: {"content_type"=>"image/png", "disposition"=>"inline;
~(省略)~
Completed 500 Internal Server Error in 1ms (ActiveRecord: 0.0ms)

TypeError (no implicit conversion of nil into String):

rack (2.1.0) lib/rack/files.rb:25:in `expand_path'
rack (2.1.0) lib/rack/files.rb:25:in `initialize'

原因

最新版のRack(2.1.0)で変更された部分が原因

Rails “TypeError (no implicit conversion of nil into String)” when loading images using image_tag, activeStorage and “has_many_attached”
Rack 2.1.0 breaks ActiveStorage #1464

解決策

次のマイナーアップデートで修正されるみたいですが取り急ぎはダウングレードすればよい
Gemfileを編集して

Gemfile
gem 'rack', '~> 2.0.8'

bundle updateで解決

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

Laragonで簡単に開発環境を構築する

皆さん、学習するとき、開発をするとき、ローカルで確認を行うときの環境構築はどうされていますか?
恐らくMacを所有している方が多くDockerを使われている方が多いと思います。

自分が現在、所有しているはHPのSPECTRE X360 13、Windows 10 Homeです。Windows 10 Homeなのです。
ラップトップと言っても非力というほどではありませんがVagrantを立ち上げている間、かなりファンが回り気になります。
それに決して速いとは言えません。さらに現時点ではまだWSL2とDocker for WSLは正式リリースされていません。

ではどうするか?
コメント 2020-01-01 215211.png
https://laragon.org/

Laragonです。
Windows向けのAMP環境構築ソフトで
PHP、Node.js、Python、Java、Go、Rubyをサポートし、
ApacheとNginxのどちらを使用するかを切り替えることができます。
では早速、インストールを行っていきましょう。

1. インストール

コメント 2020-01-01 215427.png
https://github.com/leokhoa/laragon/releases

Githubから最新版を落としてきましょう。
環境構築の経験がない、初学者の方はlaragon-full.exeを選んでいきましょう。
以後はこれをベースに進めます。

インストール先は拘りがなければわかりやすいようデフォルトのC:\Laragonにしておきます。
Laragon 2020-01-12 164745.png

VSCode等を使用する際は"Sublime Text & Terminalを追加する"のチェックが外れた状態にしておきます。
Laragon 2020-01-12 164703.png
ちなみにNotepad++、WinSCP、Putty、HeidiSQL Portable等がインストールされるのでこの1回のインストールだけで開発が出来るようになります。

2. 起動

では起動してみましょう。
Laragon 2020-01-12 165644.png
このスクリーンショットでPHP7.4になっているのは自身で後から追加ができるためです。
"ウェブ"を押すとホスト名を"localhost"ポート番号を8080で設定しているので
"http://localhost:8080/"
の形でブラウザが開きます。
"データベース"でHeidiSQL
"ターミナル"でCmderを起動します。
"ルート"は設定されたルートフォルダを表示します。"ウェブ"で表示される場所も同じものになります。
設定は右上にの歯車マークから表示できる設定で行います。

3. バージョンの追加

今回はPHPを例に紹介します。
https://windows.php.net/download/
から使用するものをダウンロードしてきます。
フォルダを解凍後、
C:\laragon\bin\php\へ追加します。
PythonであればC:\laragon\bin\python\のようになります。
Laragon 2020-01-12 170749.png
追加後に使用するバージョンを左上のメニューから選択します。
Laragon 2020-01-12 171603.png
ここから起動時に使用する言語やバージョン等を切り替えることが出来ます。

4.ガンガン使いましょう!

Macには現時点で対応していませんし、万能とまではいかないまでも
軽く使いやすいので空いた時間でサクッと作業を行うのに役立ちます。
OSがWindows 10 Homeで開発環境で悩んでいる方に特にオススメです。

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

Railsチュートリアルメモ - 第6章

メモの目次記事はこちら

Railsチュートリアル第6章へのリンク

6.1 Userモデル

Active Recordの使用方法についての章。Active RecordはたぶんRailsの中で最も核となるライブラリ。

ポイント

  • 以下コマンドでモデルを生成し、DBに反映できる
    • rails generate model User name:string email:string
    • rails db:migrate
  • Active Recordを使うことで、RailsからDBの詳細を隠蔽し、切り離すことができる。
  • SQLについて学ばずにDBに対してCRUD操作を行える。

6.2 ユーザーを検証する

バリデーションの実装方法についての章。

ポイント

  • テストクラスのsetupメソッド内に書かれた処理は、各テストが走る直前に実行される
  • modelの検証でエラーになった内容はuser.errors.full_messagesで確認できる
  • %w[]で文字列の配列を作れる e.g. %w[foo bar baz] => ["foo", "bar", "baz"]
  • callbackメソッド => ある特定の時点で呼び出されるメソッド

6.3 セキュアなパスワードを追加する

パスワードのハッシュ化保存の実装

ポイント

has_secure_passwordについて

  • Railsではhas_secure_passwordを記載するだけでパスワードのハッシュ化保存を実装できる(実際は別のライブラリを使うことが多いらしい)
  • has_secure_passwordをモデルに記載するだけで実装できるが、使用するには以下2点を満たす必要がある
    1. モデルにpassword_digest:stringを追加する必要がある
    2. 'bcrypt'gemが必要なので、Gemfileに追記してbundle installしておく
  • has_secure_passwordを追加すると、authenticateメソッドが使えるようになる

マイグレーション(カラムの追加)

  • rails generate migration add_password_digest_to_users password_digest:string
    • rails generate migrationで指定するマイグレーション名の最後に_to_usersを付与しておくと、usersテーブルへのカラム追加だと判断してファイルを作成してくれる。

基本文法

  • 多重代入 (Multiple Assignment)
    • 次のような文法で2箇所に同時に同じ値を代入できる
      • @user.password = @user.password_confirmation = "a" * 5
  • モデルのvalidateの文法
    • validates :name, presence: true, length: { maximum: 50 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヒアドキュメント【Ruby】

今回はRuby技術者認定試験で意外と狙われるヒアドキュメントについて書きます。

そもそもヒアドキュメントとは?

ヒアドキュメントは改行コードを含めて複数行の文字列を書くことができます。

ヒアドキュメントを使わない場合。

puts "ヒアドキュメントを使わない場合はこのようにクオーテーションで囲い、\n改行したい箇所に\\nを書くことで改行できる。\nちなみにバックスラッシュを二つ使う\\ことで改行(\\n)をエスケープできる"

【実行結果】

ヒアドキュメントを使わない場合はこのようにクオーテーションで囲い、
改行したい箇所に\nを書くことで改行できる。
ちなみにバックスラッシュを二つ使う\\ことで改行(\n)をエスケープできる
=> nil

ただしヒアドキュメントを使うと

here.rb
doq = <<EOS
昨日は雨

今日は晴れ

明日は曇りらしい
EOS
puts doq

【実行結果】

昨日は雨

今日は晴れ

明日は曇りらしい

上記のように\nを書かずに改行の文字列を記載することができます。

ちなみに習慣で識別子にEOSとEOLがよく使われるそうです。

意味はEOSは(end of string)、EOL(end of line)の略です。

他の文字を使っても大丈夫です。

というより、識別子に指定する文字列はヒアドキュメントの内容が理解できるものの方が良いそうです。

here.rb
doq = <<WEATHER
昨日は雨

今日は晴れ

明日は曇りらしい
WEATHER

puts doq
p doq

【実行結果】

昨日は雨

今日は晴れ

明日は曇りらしい
"昨日は雨\n\n今日は晴れ\n\n明日は曇りらしい\n"

<<-

<<-はスペースを書く場合やメソッドチェーンにするときに使います。

here.rb
doq = <<-WEATHER.lines #linesは各行を配列にするstringsのメソッド
  昨日は雨

 今日は晴れ

明日は曇りらしい
WEATHER

  昨日は雨

 今日は晴れ

明日は曇りらしい
["  昨日は雨\n", "\n", " 今日は晴れ\n", "\n", "明日は曇りらしい\n"]

""

ダブルクォートを使うことで式展開ができる

here.rb
a = '雨'
b = '晴れ'
c = '曇り'
doq = <<"WEATHER"
昨日は#{a}

今日は#{b}

明日は#{c}らしい
WEATHER
puts doq
p doq

【実行結果】

昨日は雨

今日は晴れ

明日は曇りらしい
"昨日は雨\n\n今日は晴れ\n\n明日は曇りらしい\n"

エラー

ちなみに下記はエラーになる

here.rb
doq = <<WEATHER
昨日は雨

今日は晴れ

明日は曇りらしい
 WEATHER
puts doq
p doq

【実行結果】

here.rb:4: can't find string "WEATHER" anywhere before EOF

原因は終端のWEATHERにスペースがあるから。

これを解決するにはスペースをなくすか、<<-にする

here.rb
doq = <<-WEATHER
昨日は雨

今日は晴れ

明日は曇りらしい
 WEATHER
puts doq
p doq

【実行結果】

昨日は雨

今日は晴れ

明日は曇りらしい
"昨日は雨\n\n今日は晴れ\n\n明日は曇りらしい\n"

以上がヒアドキュメントでした。

参考

今回は参考図書にRuby技術者認定試験合格教本(Silver/Gold対応)Ruby公式資格教科書を使用しました。

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

resourcesを多段階ネストさせた時のform_forメソッド

概要

resourcesを多段階ネストさせた時のform_forメソッドの使用方法に関する記事があまりなかったので、備忘録として纏めます。

スポット(Post)ごとに写真(Image)が投稿でき、写真ごとにコメント(Comment)が投稿できる旅行カタログアプリケーションを作成しております。

ビューはHamlで書いております。

作成途中のため、細かいところはご容赦くださいませ。

ルーティング

routes.rb
  resources :posts, only: [:index, :new, :create, :edit, :update] do
    resources :images, only: [:index, :new, :create, :show] do
      resources :comments, only: [:index, :create]
    end
  end

コントローラ

1つ目のネストのimageは下記の通り。

images_controller.rb
class ImagesController < ApplicationController
  before_action :set_post

  def index
    @images = @post.images.includes(:user)
  end

  def new
    @image = Image.new
  end

  def create
    @image = @post.images.new(image_params)
    if @image.save
      redirect_to post_images_path(@post)
    else
      @images = @post.images.includes(:user)
      render :index
    end
  end

  private

  def image_params
    params.require(:image).permit(:text, :image).merge(user_id: current_user.id)
  end

  def set_post
    @post = Post.find(params[:post_id])
  end

end

2つ目のネストのcommentは下記となる。
PostのIDを定義する必要があることに気付くのに時間がかかりました・・・

comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_image

  def index
    @post = Post.find(params[:post_id])
    @comment = Comment.new
    @comments = @image.comments.includes(:user)
  end

  def create
    @post = Post.find(params[:post_id])
    @comment = @image.comments.new(comment_params)
    if @comment.save
      redirect_to post_image_comments_path(@post, @image)
    else
      @comments = @image.comments.includes(:user)
      render :index
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:content).merge(user_id: current_user.id)
  end

  def set_image
    @image = Image.find(params[:image_id])
  end
end

モデル

image.rb
class Image < ApplicationRecord
  belongs_to :post
  belongs_to :user
  has_many :comments
  validates :image, presence: true
end
comment.rb
class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :user
  validates :content, presence: true
end

ビュー

new-image.html.haml
.new-image__form
  = form_for [@post, @image] do |f|
    = f.label :image, class: 'form__image' do
      = icon('fas', 'image', class: 'icon')
      = f.file_field :image, class: 'hidden'
    .new-image__form__mask
      = f.text_field :text, class: 'form__text', placeholder: 'type a caption'
    = f.submit 'Send', class: 'form__submit'
new-comment.html.haml
.comments__content__list__form
  = form_for [@post, @image, @comment ] do |f|
    .comments__content__list__form__new-comment
      .comments__content__list__form__new-comment__input-box
        = f.text_field :content, class: 'new-comment__input-box__text', placeholder: 'コメントを追加...', inputtype: "text", style:"border:none"
      = f.submit 'Send', class: 'new-comment__submit-btn'

2段ネストした場合のform_forの引数は、親、子、孫の関係になるため、post, image, commentの3つを渡しています。

以上です

間違いあればご指摘お願いいたします!

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

RSpecで言語非依存の壊れにくいバリデーションテストを作る

はじめに

RSpecでバリデーションのテストを書くとき、下記のように書くことが多いと思います。

spec/models/user_spec.rb
  it "is invalid without a name" do
    user = User.new(name: nil)
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end

かのEverydayRailsにも同様のサンプルが掲載されています。

しかし、I18nを導入してエラーメッセージなどを日本語化したときにこのテストは通過しなくなってしまいます。
そこで、次の様に修正します。

spec/models/user_spec.rb
  it "is invalid without a name" do
    user = User.new(name: nil)
    user.valid?
    expect(user.errors[:name]).to include("を入力してください")
  end

これでテストを通過することが出来ました。

しかしこれでは言語設定を変更するたびにテストが壊れて修正が必要になってしまうという問題が残ります。

解決方法

I18nの導入

ここでは記述しません。
[初学者]Railsのi18nによる日本語化対応などが参考になると思います。

I18n.translate メソッドを使用する

Railsガイド-基本的な参照、スコープ、ネストしたキーの項によると、次のようにしてエラーメッセージを呼び出すことが出来ます。

I18n.translate "activerecord.errors.messages.record_invalid"

アプリケーションのlocaleを日本に指定している状態で、rails cで実行すると

>> I18n.translate "activerecord.errors.messages.record_invalid"
=> "バリデーションに失敗しました: %{errors}"

この様になります。
冒頭のバリデーションテストをこの方法で書き換えると次のようになります。

spec/models/user_spec.rb
  it "is invalid without a name" do
    user = User.new(name: nil)
    user.valid?
    expect(user.errors[:name]).to include(I18n.t('errors.messages.blank'))
  end

上記ではI18n.translateのエイリアスであるI18n.tを代わりに使用しています。
これで、言語設定が変わっても壊れないバリデーションテストを書くことができました。

エラーメッセージの呼び出し方色々

先程も参照したRailsガイド-基本的な参照、スコープ、ネストしたキーの項によると、下記の呼び出しはすべて等価ですので、好みの方法を使って書いてみてください。

I18n.t 'activerecord.errors.messages.record_invalid'
I18n.t 'errors.messages.record_invalid', scope: :activerecord
I18n.t :record_invalid, scope: 'activerecord.errors.messages'
I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]

他のエラーメッセージを呼び出すには

Railsガイド-エラーメッセージ内での式展開の項に表でまとめられています。

I18n.translate "activerecord.errors.messages.record_invalid"record_invalidの部分を表のメッセージ列の値に置き換えることで他のエラーメッセージを呼び出すことが出来ます

引数付きのエラーメッセージ呼び出し

rails cI18n.t('errors.messages.too_short')を実行すると

>> I18n.t('errors.messages.too_short')
=> "は%{count}文字以上で入力してください"

このような式展開が使われていることから引数にcount: [最低文字数]を付加すれば良いと分かります。

>> I18n.t('errors.messages.too_short', count: 6)
=> "は6文字以上で入力してください"

以上からパスワード作成時などに6文字以上のバリデーションを設定しているときは、次のようにテストすることが出来ます。

spec/models/user_spec.rb
  it "is invalid without a shoot password" do
    user = User.new(password: 'a' * 5)
    user.valid?
    expect(user.errors[:password]).to include(I18n.t('errors.messages.too_short', count: 6))
  end

Railsガイド-エラーメッセージ内での式展開の項の表の式展開列に使用されている式展開が書かれているのでご参照ください。

おわりに

I18nで日本語化した時に、バリデーションテストでエラーメッセージをベタ書きしてチェックする方法以外のやり方を調べましたが、ピンポイントで書かれている記事がなかったため書いてみました。
同じ疑問を持った誰かのお役に立てればと思います。

参考

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

Rails超基本コマンドチートシート(モデル/ビュー/コントローラーの追加)

Ruby/Rails エンジニアとして働き始め2ヶ月ほど立ちますが、まだ Rails のコマンドを業務では使ったことがありませんのでほとんど忘れてしまっています。就職前に学習したので調べれば思い出せるとはいえ、この程度のコマンドならば、業務で必要になった時にわざわざ1つずつ調べるのは時間がかかりすぎます。
そこで、必要な時にサクッと思い出せるよう、チートシートとしてにまとめます。

前提

各コマンドの意味や内容は割愛しますので、詳細は、 Rails ガイド(https://railsguides.jp/) などをお読みください。

モデル関連

モデル生成

モデルとマイグレーション生成する

$ rails generate model Name column_name:column_type

$ rails generate model User name:string number:integer

マイグレーション実行

マイグレーションを実行する

$ rails db:migrate

カラム追加

$ rails generate migration AddColumunNameToModelName

$ rails generate migration AddAccountIdToUsers

コントローラー・ビュー関連

 コントローラー生成

コントローラーとビューが生成する

$ rails generate controller Name action

$ rails generate controllser Users index show new edit

StrongParameters

モデルのインスタンスにどのパラメーターを保存してよいかを指定する

1つのパラーメーターの場合

def create
  User.create(params[:name])
end

複数のパラメーターの場合

def create
  User.create(user_params)
end

private
def user_params
  params.permit(:name, :number)
end

ルーティング関連

テーブル名とアクション名を指定する

resource :table_name, only:[:action]



resources :users, only: [:index, :show, :new, :create, :edit, :update, :destroy]

まとめ

その他にも多用するコマンドがあれば、随時追加していきます

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

はじめてAWSでデプロイする方法⑥(EC2サーバーにAppをクローンしてアップロード)

前回までの記事

はじめてAWSでデプロイする方法①(インスタンスの作成)
はじめてAWSでデプロイする方法②(Elastic IPの作成と紐付け)
はじめてAWSでデプロイする方法③(AWSセキュリティグループの設定)
はじめてAWSでデプロイする方法④(EC2インスンタンスにSSHログイン)
はじめてAWSでデプロイする方法⑤(EC2の環境構築、Ruby, MySQL)

EC2インスタンス(サーバー)を作成し、パブリックIPをElastic IPで固定。
一般ユーザーがアクセスできるように、セキュリティグループの設定を追加(入り口を作成)
IDとPWを使って、EC2にログインして、環境構築をしました。

ざっくり説明すると、こんなところです。

今回はWEB AppをEC2インスタンスにアップロードしていきます。

WEB AppをEC2にクローンする

現段階

EC2サーバにアプリケーションのコードをクローンしようとしてもpermission deniedとエラーが出てしまいます。

原因

Githubから見てこの許可していないEC2インスタンスを拒否する

対策

EC2インスタンスのSSH公開鍵をGithubに登録する。

SSH鍵をGithubに登録すると、Githubはそれを認証してクローンを許可をだす

作業

EC2サーバのSSH鍵ペアを作成

  1. EC2にログイン
  2. キーペア作成のためコマンドを入力
[ec2-user@ip-172-31-23-189 ~]$ ssh-keygen -t rsa -b 4096

3.下記が表示されるので、エンターを押す

Enter file in which to save the key (/home/ec2-user/.ssh/id_rsa):

4.さらにエンターを押す(2回)

Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 

これで下記の表示ができれば、成功してます。

Your identification has been saved in /home/ec2-user/.ssh/id_rsa.
Your public key has been saved in /home/ec2-user/.ssh/id_rsa.pub.
The key fingerprint is:
3a:8c:1d:d1:a9:22:c7:6e:6b:43:22:31:0f:ca:63:fa ec2-user@ip-172-31-23-189
The key's randomart image is:
+--[ RSA 4096]----+
|    +            |
| . . =           |
|  = . o .        |
| * o . o         |
|= *     S        |
|.* +     .       |
|  * +            |
| .E+ .           |
| .o              |
+-----------------+

5.SSH公開鍵を表示し、値をコピーするため、下記コマンドを実装

[ec2-user@ip-172-31-23-189 ~]$ cat ~/.ssh/id_rsa.pub

6.catで表示させた公開鍵(長いテキスト)をコピー
スクリーンショット 2020-01-12 11.44.25.png

コピーした公開鍵をGithubにアクセスして登録する

  1. https://github.com/settings/keysにアクセス

スクリーンショット 2020-01-12 11.54.04.png
2. 画面右上の緑色の『 NEW SSH KEY 』をクリック
Image from Gyazo
3. タイトルを記入する(なんでも可能)
スクリーンショット 2020-01-12 12.00.39.png
4. 公開鍵(ssh-rsaから)を貼り付け
スクリーンショット 2020-01-12 12.00.56.png
エラー「Key is invalid. You must supply a key in OpenSSH public key format」が表示された場合、
貼り付けたコードに『 ssh-rsa 』が含まれているかご確認ください
スクリーンショット 2020-01-12 12.00.08.png
5. 『 Add SSH KEY 』をクリックして保存。
6. GithubのPWを入力
スクリーンショット 2020-01-12 11.59.26.png
7. 完了
スクリーンショット 2020-01-12 11.52.46.png
8. 登録できているか確認

[ec2-user@ip-172-31-23-189 ~]$ ssh -T git@github.com

下記の表示が出た場合: 『 yes 』を選択

The authenticity of host 'github.com (IP ADDRESS)' can't be established.
RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
Are you sure you want to continue connecting (yes/no)?

この際に

Warning: Permanently added the RSA host key for IP address '52.111.11.11' to the list of known hosts.

と表示された場合は, EC2に入り直しましょう。更新されたのでエラーなく入れます。
成功すると、下記の表示になるはずです。

または、下記が表示された場合: 『 yes 』を選択

The authenticity of host 'github.com (IP ADDRESS)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)?

成功すると下記の表示が出る

[ec2-user@ip-172-31-23-189 ~]$ ssh -T git@github.com
Hi <Githubユーザー名>! You've successfully authenticated, but GitHub does not provide shell access.

参考:GitHub公式

App側でUnicornのインストール

EC2にGit クローンする前に、準備としてUnicornをインストールさせましょう
Gemfileにgem'unicorn'を追加

Gemfile.
group :production do
  gem 'unicorn', '5.4.1'
end

bundle installでインストール

$ bundle install

config/unicorn.rbを作成
スクリーンショット 2020-01-12 19.02.45.png
追加したunicorn.rbに下記を記述

unicorn.rb
app_path = File.expand_path('../../', __FILE__)

#アプリケーションサーバの性能を決定する
worker_processes 1

#アプリケーションの設置されているディレクトリを指定
working_directory app_path

#Unicornの起動に必要なファイルの設置場所を指定
pid "#{app_path}/tmp/pids/unicorn.pid"

#ポート番号を指定
listen 3000

#エラーのログを記録するファイルを指定
stderr_path "#{app_path}/log/unicorn.stderr.log"

#通常のログを記録するファイルを指定
stdout_path "#{app_path}/log/unicorn.stdout.log"

#Railsアプリケーションの応答を待つ上限時間を設定
timeout 60

#以下は応用的な設定なので説明は割愛

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

production.rbを開き、下記の記述をコメントアウトする

config/environments/production.rb
config.assets.js_compressor = :uglifier
config/environments/production.rb
#config.assets.js_compressor = :uglifier

アプリケーションの保存先となるディレクトリを作成

ディレクトリの作成

#/var/wwwディレクトリを作成(後述するCapistranoの初期値がwwwなので、ディレクトリをwwwに設定しています)
[ec2-user@ip-172-31-23-189 ~]$ sudo mkdir /var/www/

作成したディレクトリをchownコマンドで権限設定

#作成したwwwディレクトリの権限をec2-userに変更
[ec2-user@ip-172-31-23-189 ~]$ sudo chown ec2-user /var/www/

作成したディレクトリに移行

[ec2-user@ip-172-31-23-189 ~]$ cd /var/www/

git clone でAppをEC2にダウンロード

GithubからGit cloneするためのリポジトリURLを取得
スクリーンショット 2020-01-12 21.42.36.png

git clone で作成したディレクトリにappをクローン

[ec2-user@ip-172-31-23-189 www]$ git clone リポジトリURL

Githubのアカウント名とPWを入力し、
ダウロードが開始される

remote: Enumerating objects: 298, done.
remote: Counting objects: 100% (298/298), done.
remote: Compressing objects: 100% (190/190), done.
remote: Total 298 (delta 109), reused 274 (delta 86), pack-reused 0
Receiving objects: 100% (298/298), 58.53 KiB | 365.00 KiB/s, done.
Resolving deltas: 100% (109/109), done.

完了

これで、EC2にAppがクローンされています。

次回はEC2にgemをインストールと設定の変更

次回の記事はこちら
はじめてAWSでデプロイする方法⑦(EC2サーバーにAppをクローンしてアップロード)

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

#Rspec + #Ruby でhelper メソッドを定義して共通化する

Globalなメソッドではなくちゃんと describe / context の中に作用を閉じ込めてくれるみたいだ

# https://relishapp.com/rspec/rspec-core/v/3-8/docs/helper-methods/arbitrary-helper-methods
#
# Arbitrary helper methods
#
# You can define methods in any example group using Ruby's def keyword or
# define_method method. These helper methods are exposed to examples in the
# group in which they are defined and groups nested within that group, but not
# parent or sibling groups.

# PASS
RSpec.describe "flat define method" do
  def help
    :available
  end

  it do
    expect(help).to be(:available)
  end
end

# PASS
RSpec.describe "define method and use in child describe" do
  def help
    :available
  end

  describe do
    it do
      expect(help).to be(:available)
    end
  end
end

# ERROR
RSpec.describe "define method chuld describe and use it in parent describe" do
  describe do
    def help
      :available
    end
  end

  # NameError:
  # undefined local variable or method `help' for #<RSpec::ExampleGroups::AnExample_3:0x00007f95d5a6c9e0>
  it do
    expect(help).to be(:available)
  end
end

# ERROR
RSpec.describe "not defined method in describe" do
  # has access to methods defined in its group (FAILED - 1)
  it do
    expect(help).to be(:available)
  end
end

# PASS
# OTHER USAGE
RSpec.describe "expectation in helper" do
  def expect_true_is_true
    expect(true).to eq true
  end

  it do
    expect_true_is_true
  end
end

# $ rspec /Users/yumainaura/.ghq/github.com/YumaInaura/YumaInaura/rspec/def-helper-method.rb

# flat define method
#   is expected to equal :available

# define method and use in child describe

#     is expected to equal :available

# define method chuld describe and use it in parent describe
#   example at /Users/yumainaura/.ghq/github.com/YumaInaura/YumaInaura/rspec/def-helper-method.rb:44 (FAILED - 1)

# not defined method in describe
#   example at /Users/yumainaura/.ghq/github.com/YumaInaura/YumaInaura/rspec/def-helper-method.rb:52 (FAILED - 2)

# expectation in helper
#   is expected to eq true

# Failures:

#   1) define method chuld describe and use it in parent describe
#      Failure/Error: expect(help).to be(:available)

#      NameError:
#        undefined local variable or method `help' for #<RSpec::ExampleGroups::DefineMethodChuldDescribeAndUseItInParentDescribe:0x00007fd91e12d230>
#      # /Users/yumainaura/.ghq/github.com/YumaInaura/YumaInaura/rspec/def-helper-method.rb:45:in `block (2 levels) in <top (required)>'

#   2) not defined method in describe
#      Failure/Error: expect(help).to be(:available)

#      NameError:
#        undefined local variable or method `help' for #<RSpec::ExampleGroups::NotDefinedMethodInDescribe:0x00007fd91e17e518>
#      # /Users/yumainaura/.ghq/github.com/YumaInaura/YumaInaura/rspec/def-helper-method.rb:53:in `block (2 levels) in <top (required)>'

# Finished in 0.00882 seconds (files took 0.16091 seconds to load)
# 5 examples, 2 failures

# Failed examples:

# rspec /Users/yumainaura/.ghq/github.com/YumaInaura/YumaInaura/rspec/def-helper-method.rb:44 # define method chuld describe and use it in parent describe
# rspec /Users/yumainaura/.ghq/github.com/YumaInaura/YumaInaura/rspec/def-helper-method.rb:52 # not defined method in describe

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2933

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

Rspec の expect と with で ネストの深いハッシュや引数をゆるくテストする / #Ruby #Rspec #Rails

Complexed hash fuzzy match

  example 'complexed match' do
    expect(SomeClass).to receive(:call).with(
      'X',
      hash_including(
        y1: 'Y1',
        y2: (be_a String),
        y4: hash_including(
          y4_1: array_including(1, 3),
          y4_3: contain_exactly(9, 7, 8),
        )
      ),
      any_args
    )
    subject
  end

Example

# Doc
# https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/setting-constraints/matching-arguments

class SomeClass
  def self.call(x, y, z)
  end
end

describe 'simple case' do
  subject do
    SomeClass.call('X','Y','Z')
  end

  it 'calls with exactly multiple args' do
    expect(SomeClass).to receive(:call).with('X', 'Y', 'Z')
    subject
  end
end

describe 'complexed case' do
  subject do
    SomeClass.call(
      'X',
      {
        y1: 'Y1',
        y2: 'Y2',
        y3: 'Y3',
      },
      'Z'
    )
  end

  example 'exactly match' do
    expect(SomeClass).to receive(:call).with(
      'X',
      {
        y1: 'Y1',
        y2: 'Y2',
        y3: 'Y3',
      },
      'Z'
    )
    subject
  end

  it 'partly match with hash in one arg' do
    expect(SomeClass).to receive(:call).with(
      'X',
      hash_including(
        y1: 'Y1',
        y2: 'Y2',
      ),
      'Z'
    )
    subject
  end
end

describe 'random value case' do
  subject do
    SomeClass.call(
      'X',
      {
        y1: 'Y1',
        y2: rand(999_999).to_s,
        y3: rand(999_999),
      },
      'Z'
    )
  end

  example 'fuzzy match on one arg' do
    expect(SomeClass).to receive(:call).with(
      'X',
      any_args,
      'Z'
    )
    subject
  end

  example 'exactly match and expect anything value' do
    expect(SomeClass).to receive(:call).with(
      'X',
      {
        y1: anything,
        y2: anything,
        y3: anything,
      },
      'Z'
    )
    subject
  end

  example 'exactly match and expect anything value' do
    expect(SomeClass).to receive(:call).with(
      'X',
      {
        y1: (be_a String),
        y2: (be_a String),
        y3: (be_a Integer),
      },
      'Z'
    )
    subject
  end

  example 'partly fuzzy match' do
    expect(SomeClass).to receive(:call).with(
      'X',
      hash_including(
        y1: 'Y1',
        y2: (be_a String),
      ),
      any_args
    )
    subject
  end
end

describe 'deep conplexed case' do
  subject do
    SomeClass.call(
      'X',
      {
        y1: 'Y1',
        y2: rand(999_999).to_s,
        y3: rand(999_999),
        y4: {
          y4_1: [1,2,3],
          y4_2: [4,5,6],
          y4_3: [7,8,9],
        }
      },
      'Z'
    )
  end

  example 'complexed match' do
    expect(SomeClass).to receive(:call).with(
      'X',
      hash_including(
        y1: 'Y1',
        y2: (be_a String),
        y4: hash_including(
          y4_1: array_including(1, 3),
          y4_3: contain_exactly(9, 7, 8),
        )
      ),
      any_args
    )
    subject
  end
end

# $ rspec -fd /Users/yumainaura/.ghq/github.com/YumaInaura/YumaInaura/rspec/with.rb

# simple case
#   calls with exactly multiple args

# complexed case
#   exactly match
#   partly match with hash in one arg

# random value case
#   fuzzy match on one arg
#   exactly match and expect anything value
#   exactly match and expect anything value
#   partly fuzzy match

# more conplexed case
#   complexed match

# Finished in 0.01357 seconds (files took 0.15283 seconds to load)
# 8 examples, 0 failures

Ref

Matching arguments - Setting constraints - RSpec Mocks - RSpec - Relish
https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/setting-constraints/matching-arguments#basic-example

image

Ref

使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」 - Qiita
https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2932

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

#Rspec + #Ruby / Complexed Nested Hash arguments / Fuzzy match / Use expect receive syntax and "with" matcher

Complexed hash fuzzy match

  example 'complexed match' do
    expect(SomeClass).to receive(:call).with(
      'X',
      hash_including(
        y1: 'Y1',
        y2: (be_a String),
        y4: hash_including(
          y4_1: array_including(1, 3),
          y4_3: contain_exactly(9, 7, 8),
        )
      ),
      any_args
    )
    subject
  end

Example

# Doc
# https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/setting-constraints/matching-arguments

class SomeClass
  def self.call(x, y, z)
  end
end

describe 'simple case' do
  subject do
    SomeClass.call('X','Y','Z')
  end

  it 'calls with exactly multiple args' do
    expect(SomeClass).to receive(:call).with('X', 'Y', 'Z')
    subject
  end
end

describe 'complexed case' do
  subject do
    SomeClass.call(
      'X',
      {
        y1: 'Y1',
        y2: 'Y2',
        y3: 'Y3',
      },
      'Z'
    )
  end

  example 'exactly match' do
    expect(SomeClass).to receive(:call).with(
      'X',
      {
        y1: 'Y1',
        y2: 'Y2',
        y3: 'Y3',
      },
      'Z'
    )
    subject
  end

  it 'partly match with hash in one arg' do
    expect(SomeClass).to receive(:call).with(
      'X',
      hash_including(
        y1: 'Y1',
        y2: 'Y2',
      ),
      'Z'
    )
    subject
  end
end

describe 'random value case' do
  subject do
    SomeClass.call(
      'X',
      {
        y1: 'Y1',
        y2: rand(999_999).to_s,
        y3: rand(999_999),
      },
      'Z'
    )
  end

  example 'fuzzy match on one arg' do
    expect(SomeClass).to receive(:call).with(
      'X',
      any_args,
      'Z'
    )
    subject
  end

  example 'exactly match and expect anything value' do
    expect(SomeClass).to receive(:call).with(
      'X',
      {
        y1: anything,
        y2: anything,
        y3: anything,
      },
      'Z'
    )
    subject
  end

  example 'exactly match and expect anything value' do
    expect(SomeClass).to receive(:call).with(
      'X',
      {
        y1: (be_a String),
        y2: (be_a String),
        y3: (be_a Integer),
      },
      'Z'
    )
    subject
  end

  example 'partly fuzzy match' do
    expect(SomeClass).to receive(:call).with(
      'X',
      hash_including(
        y1: 'Y1',
        y2: (be_a String),
      ),
      any_args
    )
    subject
  end
end

describe 'deep conplexed case' do
  subject do
    SomeClass.call(
      'X',
      {
        y1: 'Y1',
        y2: rand(999_999).to_s,
        y3: rand(999_999),
        y4: {
          y4_1: [1,2,3],
          y4_2: [4,5,6],
          y4_3: [7,8,9],
        }
      },
      'Z'
    )
  end

  example 'complexed match' do
    expect(SomeClass).to receive(:call).with(
      'X',
      hash_including(
        y1: 'Y1',
        y2: (be_a String),
        y4: hash_including(
          y4_1: array_including(1, 3),
          y4_3: contain_exactly(9, 7, 8),
        )
      ),
      any_args
    )
    subject
  end
end

# $ rspec /Users/yumainaura/.ghq/github.com/YumaInaura/rspec/with.rb

# simple case
#   calls with exactly multiple args

# complexed case
#   exactly match
#   partly match with hash in one arg

# random value case
#   fuzzy match on one arg
#   exactly match and expect anything value
#   exactly match and expect anything value
#   partly fuzzy match

# Finished in 0.01604 seconds (files took 0.15593 seconds to load)
# 7 examples, 0 failures

Ref

Matching arguments - Setting constraints - RSpec Mocks - RSpec - Relish
https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/setting-constraints/matching-arguments#basic-example

image

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2931

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

libssl.1.0.0.dylibがないことでエラーになった時の対処法 dyld: lazy symbol binding failed: Symbol not found: _SSL_library_init

Railsを久しぶりに起動しようとしたらなんかエラーが起きていた。

masashi-no-mbp:MarimoKing pcuser$ rails s
dyld: lazy symbol binding failed: Symbol not found: _SSL_library_init
  Referenced from: /Users/pcuser/.rbenv/versions/2.6.0/lib/ruby/2.6.0/x86_64-darwin18/openssl.bundle
  Expected in: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib

結果としては、rbenvで入れていたRubyのリンクの設定が原因でダメだったみたい。

対処法

Rubyを入れ直すことで解消した。

$ rbenv uninstall 2.6.0 && rbenv install 2.6.0

参考リンク

[MEMO] gem コマンドで Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib が出たときの対処法

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

Procのススメ(番外編)

はじめに

Ruby Advent Calendar 2020 の13日目の記事です。

昨日は、@jerrywdlee さんの Rubyでロックファイルによる簡易的排他制御 でした。

昨年の11/14に開催された平成Ruby会議で、「Procのススメ」というテーマで登壇したんですが、その番外編を1年以上経った 今更 、Advent Calendarという機会に便乗して書きます。
当初発表内容に盛り込もうと思っていたけど、時間の都合で省いたトピックについて記事にまとめていきます。

※1. 知識はあるけど自分でもなかなか利用場面に恵まれないテクニックが結構あることは内緒 :secret:
※2. 時間の都合上、一部に関しては「使ってみる」 の部分は用意できませんでした :pray:

1. Enumerator::Yielder#to_proc

発表当時の構想にはなかったんですが、次の「独自に to_proc を定義」の部分の都合上、Ruby2.7 がリリースされた現在だと先に説明しておいた方が良さそうなので最初に説明しておきます。

Enumerator::Yielder#to_procとは

Enumearator::Yielder#to_proc は Ruby2.7 から追加されたメソッドです。
Enumearator::Yielder 自体がかなりマニアックなので、るりまのサンプルコードを見ながら説明していきます。

text = <<-END
Hello
こんにちは
END

enum = Enumerator.new do |y|
  text.each_line(&y)
end

enum.each do |line|
  p line
end
# => "Hello\n"
#    "こんにちは\n"

上記のコードの new に渡しているブロックのブロック引数で渡ってくるのが Enumerator::Yielder のインスタンスです。

enum = Enumerator.new do |y| # <= こいつが `Enumerator::Yielder`
  text.each_line(&y)
end

この部分を &修飾子to_proc を利用しない等価なコードを書くと以下の様になります。

enum = Enumerator.new do |y|
  text.each_line { |line| y.yield line } # <= y.yield でブロックを評価する際に line をブロック引数として渡す
end

また、Enumeratoreach でイテレーション(繰り返し処理)を実行する際に渡されるブロックが Enumerator::Yielder としてブロック引数で渡ってきます。

enum.each do |line| # <= このブロックの部分が `Enumerator::Yielder` として渡ってくる
  p line
end

つまり、要素をブロック引数にそのまま引き渡すのが Enumerator::Yielder#to_proc です。

2. 独自に to_proc を定義

&修飾子と to_proc

平成Ruby会議の際のスライドにある通り、実引数の最後で &修飾子 を使うとその実引数に対して to_proc が呼ばれます。

thumbnail

Procのススメ/recommendation-of-proc - Speaker Deck

デフォルトで to_proc が定義されているのはスライドに記載している Proc(lambda)MethodSymbolHash、それに Ruby2.7 からは先程紹介したEnumerator::Yielder が加わります。
それ以外は普通は例外が発生します。

[1, 2, 3].map(&"to_proc")
# => TypeError (wrong argument type String (expected Proc))

ただ、以下の条件を満たせば他のクラスでも &修飾子 が利用できます。

  • to_proc が定義されている
  • to_procProc(lambda) を返す
class String
  def to_proc
    -> (n) { "#{self.upcase}#{n}" }
  end
end

[1, 2, 3].map(&"to_proc")
# => ["TO_PROC1", "TO_PROC2", "TO_PROC3"]

使ってみる

to_proc を実装するテクニックを使って簡単なロガーを作ってみます。
出力対象は以下のコードです。(出力対象の設計はめちゃくちゃ適当なのでご容赦を :pray: )

module Readable;end

module Writable;end

class User
  @@role = 'general'
  include Readable

  def initialize(name)
    @name = name
  end

  def self.all;end

  def name;end
end

class Admin < User
  @@role = 'admin'
  ADDRESS = 'Tokyo'
  include Writable

  def manage;end
end

ロガーの実装は以下の通り。
デバッグ情報を色々ぶち込んだハッシュを pp で出力します。
また、今回はインスタンスではなく Logger クラスを直接渡す実装にしてみました。
&修飾子 をつける実引数が to_proc を実装していればいいので、 to_proc を特異メソッドにすれば OK です。

class Logger
  def self.to_proc
    -> (obj) do
      klass = obj.class
      pp(
        {
          inspect: obj.inspect,
          class: klass.name,
          ancestors: klass.ancestors,
          class_variables: klass.class_variables,
          constants: klass.constants,
          included_modules: klass.included_modules,
          singleton_methods: klass.singleton_methods(false),
          instance_methods: klass.instance_methods(false)
        }
      )
    end
  end
end

あとは each&修飾子 をつけた Logger を渡してやれば OK です。

[Admin.new("Alice"), User.new("Bob")].each(&Logger)
# => {:inspect=>"#<Admin:0x00007ff6c8892bc0 @name=\"Alice\">",
#     :class=>"Admin",
#     :ancestors=>[Admin, Writable, User, Readable, Object, Kernel, BasicObject],
#     :class_variables=>[:@@role],
#     :constants=>[:ADDRESS],
#     :included_modules=>[Writable, Readable, Kernel],
#     :singleton_methods=>[],
#     :instance_methods=>[:manage]}
#    {:inspect=>"#<User:0x00007ff6c8892b70 @name=\"Bob\">",
#     :class=>"User",
#     :ancestors=>[User, Readable, Object, PP::ObjectMixin, Kernel, BasicObject],
#     :class_variables=>[:@@role],
#     :constants=>[],
#     :included_modules=>[Readable, PP::ObjectMixin, Kernel],
#     :singleton_methods=>[:all],
#     :instance_methods=>[:name]}

.irbrc とか .pryrc にこんな感じの便利クラスを用意しておくと便利(かも?)

3. 関数合成(Proc#>>, Proc#<<, Method#>>, Method#<<)

関数合成について

例として、別々の処理をする lambda を返すメソッドが3つあり、それを配列に対して適用していきたいケースを考えます。

def convert_integer
  -> (n) { n.to_i }
end

def count_up
  -> (n) { n.succ }
end

def output
  -> (n) { p n }
end

愚直に each で実装すると次のようになります。

%w[1 2 3].each do |n|
  output.call(count_up.call(convert_integer.call(n)))
end

これを関数合成を使うと以下のように書けます。

%w[1 2 3].each(&convert_integer >> count_up >> output)

配列の要素に対してどのような処理が適用されていくのかが左から右に流れるように読めて読みやすくなりました。:tada:
ちなみに << もあるので逆方向の関数合成をすることもできます。

%w[1 2 3].each(&output << count_up << convert_integer)

使ってみる

先程は lambda を返すメソッドを定義していましたが、実際は以下のように定義されている事が多いかと思います。

def convert_integer(n)
  n.to_i
end

def count_up(n)
  n.succ
end

def output(n)
  p n
end

また、可読性重視のためにループごとの処理を少なくし、以下のように書くこともあると思います。

%w[1 2 3]
  .map { |n| convert_integer(n) }
  .map { |n| count_up(n) }
  .each { |n| output(n) }

ただ、これだと配列の要素が多い場合にループ回数が3倍になってしまいます。
ここで関数合成が使えます。
今回は each で実行したい処理は全てメソッドなので、Method クラスを利用します。
Method にも >><< が実装されているので、関数合成ができます。
使い方・動きは Proc#>>Proc#<< と同じです。

%w[1 2 3].each(&method(:convert_integer) >> method(:count_up) >> method(:output))

4. メソッドの引数にブロック(proc)を渡す

Proc(lambda) はオブジェクトなので、引数として渡すことができます。
これをうまく使うと、メインとなる処理の前後に前処理や後処理を必要に応じて差し込むような柔軟な実装ができます。

def hoge(before: nil, after: nil)
  before.call unless before.nil?
  yield if block_given?
  after.call unless after.nil?
end

# 上記と下記は等価なコード
def hoge(before: nil, after: nil, &block)
  before.call unless before.nil?
  block.call unless block.nil?
  after.call unless after.nil?
end

実行側は下記のようになります。

hoge(
  before: -> { p 1 },
  after: -> { p 2 }
) { p 3 }
# 1
# 3
# 2
# => 2

5. Enumerator::Lazy

Enumerator::Lazy とは

るりまを見ると下記のように書いてあります。

map や select などのメソッドの遅延評価版を提供するためのクラス。
動作は通常の Enumerator と同じですが、以下のメソッドが遅延評価を行う (つまり、配列ではなく Enumerator を返す) ように再定義されています。

これはコードを見るとピンとくるかもです。
下記は単純に奇数のみの配列を返すコードです。

[1, 2, 3].select(&:odd?)
# => [1, 3]

よく見かける mapselect 等のメソッドは Enumerator クラスに定義されています。
これらのメソッドにブロック(または Proc)を渡すと、渡したブロックを評価して配列を返します。
また、ブロックを渡さなかったときは Enumerator クラスのインスタンスを返します。
普段は意識していないかもしれませんが、下記のようなコードを書くときにこの挙動を使っています。

%w[a b c].map.with_index(1) { |char, n| "#{n}: #{char}" }
# => ["1: a", "2: b", "3: c"]

一方、Enumerator::Lazy を使う場合下記のようなコードになります。

[1, 2, 3].select.lazy
# => #<Enumerator::Lazy: #<Enumerator: [1, 2, 3]:select>>

Enumerator::Lazyforce または first または to_a が呼ばれるまで値が確定せず、ループも回りません。

def hoge(x)
  x.tap { p :hoge }
end

def fuga(x)
  x.tap { p :fuga }
end

# :hogeも:fugaも1度しか出力されない
[1, 2, 3].lazy.map(&method(:hoge)).map(&method(:fuga)).take(1).to_a
# :hoge
# :fuga
# => [1]

この挙動を利用するとループ回数を減らせるのでパフォーマンス改善に利用できそうです。
ただ、残念ながら「lazy は基本的には遅くなる」そうです。(ruby-jp情報)
Enumerator::Lazy は以下のようなものに対して利用するといいそうです。(これもruby-jp情報)

  1. 大きすぎて全体をメモリに乗せられないもの 例:数GBのファイル
  2. 終わりがわからないもの 例:ネットワーク越しにやってくるデータ
  3. 終わりがないもの 例: (Date.today..)

最後に

書くと言っておきながら1年以上ずっと書けていなかった記事をようやく書けました。
今夜は赤飯です :raised_hands: (なお実際はカレー :curry: でした)

(Proc#curry について書いていればきれいにオチつけれたのにOTL)

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