20200621のRubyに関する記事は17件です。

Rubyの配列操作

map

人数が5人以上のgroupのidを配列で返すイメージ

ids = groups.map{ |group| group.id if group.count >= 5}

#=> [1, 2, nil, 4, ・・・]

条件に当てはまらない要素はnilになる

.reject(&:blank?)

配列からnilと空文字を無くす

ids = groups.map{ |group| group.id if group.count >= 5}.reject(&:blank?)

#=> [1, 2, 4, ・・・]

filter_map

上記をまとめて。Ruby2.7から利用可能

ids = groups.filter_map{ |group| group.id if group.count >= 5}

#=> [1, 2, 4, ・・・]

join

連結した文字列を返す。引数に入れた文字列を挟み込みことができる

['A','B','C'].join
#=> "ABC"

['A','B','C'].join('|')
#=> "A|B|C"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails アセットプレコンパイル時のエラー'ActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.'の解消方法

背景

  • 作成したRailsアプリケーションをデプロイしてページにアクセスしたところ、以下のエラー画面が出てアクセスできなかった。

  • そこで、log/production.rbを確認したところ、以下のエラーが出ていた
log/production.rb
ActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.):

このエラーの解決に非常に手間取ったので、正しい方法かはわからないですが、自分の環境での解決策をシェアします。

環境

Rails 5.2.4
Ruby 2.6.4

解決策

config/environments/production.rbを以下のように編集したら直った

# Do not fallback to assets pipeline if a precompiled asset is missed.
- config.assets.compile = false
+ config.assets.compile = true

参考文献

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

Ruby で gRPC のエラーハンドリング をする際のTips

Ruby で gRPC クライアントを実装する際の、エラーハンドリングのTipsをまとめる

前提

  • Ruby 2.3.8
  • Rails 5.0.7.2
  • grpc gem 1.2.8

tl;dr

  • gRPC コールに対するエラーレスポンスは GRPC::BadStatus 型の例外なので、gRPC コールする際は必ず resuce する
  • 捕捉した Exception の詳細情報を取得したい場合、 to_rpc_status メソッドで Google::Rpc::Status 型にキャストする
  • gRPC サーバーから error details が返ってきた場合、Any 型で返ってくるので、適切な型に unpack する必要がある
  • gem のエラークラス https://github.com/grpc/grpc/blob/master/src/ruby/lib/grpc/errors.rb

まずはエラーを捕捉する

gRPC コールのレスポンスは基本的には以下の Code のどれかが返ってくる。

enum Code {
  OK = 0;
  CANCELLED = 1;
  UNKNOWN = 2;
  INVALID_ARGUMENT = 3;
  DEADLINE_EXCEEDED = 4;
  NOT_FOUND = 5;
  ALREADY_EXISTS = 6;
  PERMISSION_DENIED = 7;
  UNAUTHENTICATED = 16;
  RESOURCE_EXHAUSTED = 8;
  FAILED_PRECONDITION = 9;
  ABORTED = 10;
  OUT_OF_RANGE = 11;
  UNIMPLEMENTED = 12;
  INTERNAL = 13;
  UNAVAILABLE = 14;
  DATA_LOSS = 15;
}

https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
gRPC サーバーの実装としては正常系は Code OK(0) を返し、エラーであれば OK 以外の Code(1~15) を返すよう作るかと思う。

エラーが返ってきた場合、grpc gem 側で勝手に exception を発生させるらしく、rescue しないとそこでプログラムがストップしてしまう。
なので、以下のように rescue で例外を拾ってあげる必要がある。

    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.get_hoge
    rescue GRPC::BadStatus => ex
      res = "error"
    rescue ex
      res = "unexpected error"
    end

gem で定義されている エラー class を見ると、全ての class が BadStatus class を継承していることがわかる。

実際に、発生した Exception の継承ツリーを見ると

p ex.class.ancestors
# =>[GRPC::InvalidArgument, GRPC::BadStatus, GRPC::Core::StatusCodes, StandardError, Exception...

となっており、基本的に GRPC::BadStatus で全ての Exception を捕捉できる。

もっと詳細に条件分けして処理したい場合は GRPC::InvalidArgument のような単位で拾うこともできる。

捕捉した Exception から情報を取り出す

捕捉した Exception から情報を取り出してみよう。
BadStatus インスタンスには4つのフィールドがある。
https://github.com/grpc/grpc/blob/d48d39c4324f06a6da24bb4f67e8ef21166ba65b/src/ruby/lib/grpc/errors.rb#L49-L52

例えば、Go で実装した gRPC サーバーから以下のようにエラーを作成して ruby に返してみる。

server.go
  func (s *Server) set_name(name string) {
    st := status.New(codes.InvalidArgument, "invalid username")
    return nil, st.Err()
  }
client.rb
    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.set_name("hoge1")
    rescue GRPC::BadStatus => ex
      p ex.code # => 3
      p ex.message # => "3:invalid username"
      p ex.details # => "invalid username"
      res = "error"
    rescue ex
      res = "unexpected error"
    end

Status Code 3"invalid username" というメッセージが取得できた

error details を取得したい場合は、 to_rpc_status メソッドを使う

上記までで、Status Code とエラーメッセージが取得できた。
しかし、実際に作り込む場合、エラーの詳細を渡すために error details を使うことも多いと思う。
https://christina04.hatenablog.com/entry/grpc-error-details
https://grpc.io/docs/guides/error/#richer-error-model

もし、 error details 含めた詳細な情報が欲しければ、 to_rpc_status メソッドを使うことでより詳細な情報を取得することができる。
to_rpc_status の実装は以下になるが、これを使うことで、Google::Rpc::Status 型にキャストでき、trailer メタデータを含めた詳細な情報を取り出すことができる。
https://github.com/grpc/grpc/blob/d48d39c4324f06a6da24bb4f67e8ef21166ba65b/src/ruby/lib/grpc/errors.rb#L63-L75

先ほどの実装例に、error details を追加してみた。

server.go
  func (s *Server) set_name(name string) {
    st := status.New(codes.InvalidArgument, "invalid username")

    // error details の情報を作ってセットする
    desc := "The username must only contain alphanumeric characters"
    v := &errdetails.BadRequest_FieldViolation{
        Field:       "username",
        Description: desc,
    }
    br := &errdetails.BadRequest{}
    br.FieldViolations = append(br.FieldViolations, v)
    st, _ = st.WithDetails(br)

    return nil, st.Err()
  }
client.rb
require 'google/rpc/error_details_pb'

    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.set_name("hoge1")
    rescue GRPC::BadStatus => ex
      p ex.class # => GRPC::InvalidArgument
      p ex.to_rpc_status.class # => Google::Rpc::Status
      p ex.to_rpc_status # => <Google::Rpc::Status: code: 3, message: "invalid username", details: [<Google::Protobuf::Any: type_url: "type.googleapis.com/google.rpc.BadRequest", value: "\nC\n\tusername\x126The username must only contain alphanumeric characters">]>

      ex.to_rpc_status.details.each do |detail|
        p detail.type_url # => "type.googleapis.com/google.rpc.BadRequest"
        p detail.unpack(Google::Rpc::BadRequest) # => <Google::Rpc::BadRequest: field_violations: [<Google::Rpc::BadRequest::FieldViolation: field: "username", description: "The username must only contain alphanumeric characters">]>
      end
      res = "error"
    rescue ex
      res = "unexpected error"
    end

注目すべきは details details: [<Google::Protobuf::Any: type_url: "type.googleapis.com/google.rpc.BadRequest", value: "\nC\n\tusername2\x126The username must only contain alphanumeric characters">] の部分。
これが、trailer として送られてきた error details のデータであり、なにやら Google::Protobuf::Any 型のデータと value というデータがある。
error details は複数設定でき、配列で返ってくるので、取り出すには each で回してあげる必要がある。
type_url というのは error details の型の定義場所が格納されていて、Google がデフォルトで用意した型を使用していれば、"type.googleapis.com/google.rpc.BadRequest" となる。もちろん独自で定義した型を error details としてセットすることもできるので、その場合は自分がprotoファイルで定義した場所が格納される。

また、返ってきたインスタンスは Google::Protobuf::Any 型となっていて、このままだとデータがシリアライズされた状態でうまく取り出せない。そこで、使用するのが unpack メソッド。
type_url で型を判定し、unpack で戻したい型にキャストしてあげることで、ようやく、 error details を取り出すことができた。
ちなみに、Google::Rpc::BadRequest の型定義を参照するには、google/rpc/error_details_pb をインポートしておく必要がある点に注意。

所感

error details を取り出すのに結構手間取った。
https://grpc.io/docs/languages/ruby/quickstart/ のサンプルコードにはここまで詳しく載ってないので、結局はいちいち gem の中身まで見にいったりしなければならない状況なのがちょっと辛い。

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

dockerイメージ内にyarnをインストールする

背景

Dockerを利用して作成したRailsアプリ(v5.2.4)を本番環境にデプロイする際、アセットをプリコンパイルしようとしたところ、以下のエラーがでて実行できなかった。

$docker-compose run web bundle exec rake assets:precompile RAILS_ENV=production
Starting excite-map_db_1 ... done
Yarn executable was not detected in the system.
Download Yarn at https://yarnpkg.com/en/docs/instal

解消方法

Dockerfileに以下を追加

RUN curl https://deb.nodesource.com/setup_12.x | bash
RUN curl https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update && apt-get install -y nodejs yarn postgresql-client

参考

https://github.com/yarnpkg/yarn/issues/7329

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

【hidden_field】rails hidden_field を使って情報を送ろう!!!!

【ゴール】

hidden_fieldを使って情報を送る

画面収録 2020-06-21 20.06.48.mov.gif

【メリット】

■ paramaterの理解度向上
■ hidden_fieldの使い方理解
■ MVCの理解度向上

【開発環境】

■ Mac OS catalina
■ Ruby on Rails (5.2.4.2)
■ Virtual Box:6.1
■ Vagrant: 2.2.7

【実装】

アプリケーション作成

※「scaffold」で一気に作成

mac.terminal
$ rails new hidden_lesson
$ rails g scaffold Item name:string text:text price:integer amount:integer
$ rails db:migrate 

アプリケーション調整

※ルートを追記
※homeにitemsのindexページが来るように
※postメソッドで確認画面へのルートを作成

config/routes.rb
 root 'items#index'
 post 'items/confirm' => 'items#confirm'

※views/itemsにconfirm.html.erbを作成
※items/_form.html.erbを編集
hidden_fieldでUserには見えないようにページで情報を保持します!!!

items/confirm.html.erb
<h3>Confirm page</h3>
<%= form_with model:@item do |form| %>

<div class="field">
  <%= form.label :name %>
  <%= @item.name %>
  <%= form.hidden_field :name %>
</div>

<div class="field">
  <%= form.label :text %>
  <%= @item.text %>
  <%= form.hidden_field :text %>
</div>

<div class="field">
  <%= form.label :price %>
  <%= @item.price %>
  <%= form.hidden_field :price %>
</div>

<div class="field">
  <%= form.label :amount %>
  <%= @item.amount %>
  <%= form.hidden_field :amount %>
</div>

<div class="actions">
  <%= form.submit %>
</div>
<% end %>

items/_form.html.erb
<%= form_with(model: item, local: true) do |form| %> #これを↓に変更

<%= form_with(model: item, local: true, url: items_confirm_path) do |form| %>

以上です。商品詳細画面遷移前に確認画面が挟まれているはずです。

【合わせて読みたい】

■form_withに関して
https://qiita.com/tanaka-yu3/items/50f54f5d4f4b8dfe19f3

■local: trueに関して
https://qiita.com/hayulu/items/5bf26656d7433d406ede

■確認画面作成に関して
https://qiita.com/tomoharutt/items/7959d28764912c64562f

■formの引数が2つの場合
https://qiita.com/tanaka-yu3/items/94d2b9fccc9577756127

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

fgd gdfgdf dfhdf

fhdfh dfdfhdf dfhdfhfd

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

minitestでモック、スタブする(RR、WebMock、MiniTest::Mockを使う)

この記事について

仕事で、railsのテストフレームワークにminitestを使っています。そして、railsプロジェクトの中では、モックやスタブをするときに、RR、WebMockといったgemや、minitest標準のモックであるMiniTest::Mockが使われています。
テストを書くときに、モックやスタブの書き方に戸惑うことが多くありました。
この記事では、テストダブルとはどんなものか、モックとスタブの違いはなにか、RR、WebMock、MiniTest::Mockそれぞれの使い方について記述します。

テストダブル(モック、スタブ)とは

まず、テストダブルとはどんなものでしょう。また、モック、スタブの違いはなんでしょう。

  • テストダブルとは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品(ダブルは代役、影武者を意味する)
  • モックもスタブも、テストダブルの一種

テストダブルの5つのバリエーション

テストダブルは、xUnit Test Patternの書籍によると、5つのバリエーションがあります。

1. テストスタブ

  • テスト対象コードが依存する実際のコンポーネントを置き換えるために使用する
  • テスト時の呼び出しときに、あらかじめ決められた値を返すように設定する

2. テストスパイ

  • テスト対象コードが実行された時の間接的な出力をキャプチャし、後のテストによる検証のために保存する
  • 呼び出しに基づく情報を記録するスタブ

3. モックオブジェクト

  • テスト対象コードが実行される際に、テスト対象コードからの間接的な出力を検証するために使用するオブジェクト
  • 間接出力の検証に重きが置かれる
  • 期待した呼び出しが行われたかを検証できる(どんな引数で呼ばれたか、など)

4. フェイクオブジェクト

  • テスト対象コードの依存コンポーネントを置き換えるオブジェクト
  • 依存コンポーネントと同じ機能を実装しているが、よりシンプルな方法で実装されている
  • フェイクを利用する一般的な理由は、実際の依存コンポーネントがまだ利用できない、遅すぎる、または副作用があるためにテスト環境で使用できない、などがある

5. ダミーオブジェクト

  • テスト対象コードのメソッドシグネチャの中に、パラメータとしてオブジェクトを必要とする場合に、ダミーオブジェクトを使う(テストもテスト対象コードでもこのオブジェクトを気に掛けていない場合)

私は、スタブとモックをよく混同していましたが、スタブは依存するコンポーネントを置き換えるものであり、モックはテスト対象コードからの出力が期待通りであるか検証するものである、ということを理解しました。

参考
Test Double / xUnit Patterns.com
wiki テストダブル
自動テストのスタブ・スパイ・モックの違い

RR

RRは、Rubyのテストダブルのフレームワークのgemです。
読み方は、'Double Ruby'と読むそうです。
RRにはアダプタが用意されているので、RSpec、Test::Unit、MiniTest/MiniSpecなどのテストフレームワークと統合することができるようです。

GitHub:https://github.com/rr/rr
公式:http://rr.github.io/rr/

RR is a test double framework that features a rich selection of double techniques and a terse syntax.
(訳:RRは、豊富なダブルテクニックと簡潔な構文を特徴とするテストダブルフレームワークです。)

RRの使い方

RRには、モック、スタブ、プロキシ、スパイが実装されています。
RRのGitHubページのサンプル通りですが、こんな感じで書けます。

スタブ

stubで、スタブする(実際の呼び出しを置き換える)ことができます。

# 何も返さないメソッドをスタブする
stub(object).foo
stub(MyClass).foo

# 常に値を返すスタブメソッド
stub(object).foo { 'bar' }
stub(MyClass).foo { 'bar' }

# 特定の引数で呼び出されたときに値を返すスタブメソッド
stub(object).foo(1, 2) { 'bar' }
stub(MyClass).foo(1, 2) { 'bar' }

詳細はstubのページ参照。

モック

mockで、期待した呼び出しが行われるか検証するモックを作成できます。

# メソッドが呼ばれることを期待する
# objectのfooメソッドが呼ばれることを期待する
mock(object).foo
mock(MyClass).foo

# メソッドに期待値を作成し、常に指定した値を返すようにスタブする
# objectのfooメソッドが'bar'を返すことを期待する
mock(object).foo { 'bar' }
mock(MyClass).foo { 'bar' }

# 特定の引数を持つメソッドに期待値を作成し、それを返すためにスタブを作成する
# objectのfooメソッドが引数1, 2で呼ばれ、'bar'を返すことを期待する
mock(object).foo(1, 2) { 'bar' }
mock(MyClass).foo(1, 2) { 'bar' }

詳細はmockページ参照。

スパイ

stubと、assert_receivedexpect(xxx).to have_receivedの記述を組み合わせて、スパイ(呼び出された情報を記録するスタブ)が書けるようです。
(公式GitHubには、Test::UnitとRspecでの書き方はありましたが、minitestでの書き方は載っていませんでした。)

# RSpec
stub(object).foo
expect(object).to have_received.foo

# Test::Unit
stub(object).foo
assert_received(object) {|o| o.foo }

プロキシ

proxyを使うと、メソッドを完全にオーバーライドせずにインターセプトして新しい戻り値を設定したスタブやモックが作れるようです。

# 既存のメソッドを完全にオーバーライドせずにインターセプトして
# 既存の値から新しい戻り値を取得する
stub.proxy(object).foo {|str| str.upcase }
stub.proxy(MyClass).foo {|str| str.upcase }

# 上記の例でやってることに加えて、さらに期待値のモックを作成する
mock.proxy(object).foo {|str| str.upcase }
mock.proxy(MyClass).foo {|str| str.upcase }

# クラスの新しいメソッドをインターセプトし、戻り値にダブルを定義する
stub.proxy(MyClass).new {|obj| stub(obj).foo; obj }

# 上記の例でやってることに加えて、.newに期待値のモックを作成する
mock.proxy(MyClass).new {|obj| stub(obj).foo; obj }

詳細はmock.proxystub.proxyページ参照。

クラスのインスタンス

any_instance_ofで、インスタンス作成時にメソッドをスタブしたりモックできます。また、stub.proxyを使うと、インスタンスそのものにアクセスできるようになります。

# MyClass のインスタンスの作成時にメソッドをスタブする
any_instance_of(MyClass) do |klass|
  stub(klass).foo { 'bar' }
end

# インスタンス自体にアクセスできるようにする別の方法
# MyClass.newされたインスタンスobjをスタブしている
stub.proxy(MyClass).new do |obj|
  stub(obj).foo { 'bar' }
end

詳細は#any_instance_ofページ参照。

Pureなモックオブジェクト

モックのためだけにオブジェクトを使用したい場合は、空のオブジェクトを作成することで可能です。

mock(my_mock_object = Object.new).hello

ショートカットとしてmock!を使うこともできます。

# 空の #hello メソッドを持つ新しいモックオブジェクトを作成し、そのモックを取得する
# モックオブジェクトを #subject メソッドで取得できる
my_mock_object = mock!.hello.subject

#dont_allow

#dont_allow#mockの逆で、ダブルには絶対にコールされないという期待を設定します。ダブルが実際に呼び出された場合、TimesCalledErrorが発生します。

dont_allow(User).find('42')
User.find('42') # raises a TimesCalledError

その他

RRは#method_missingを使ってメソッドの期待値を設定しているそうです。これにより、#should_receive#expectsメソッドを使う必要がありません。
また、引数の期待値を設定するために#withメソッドを使う必要がないそうです。(使いたければ使えます)

mock(my_object).hello('bob', 'jane')
mock(my_object).hello.with('bob', 'jane')  # withがついているが上と同じ

RRは、ブロックを使って戻り値を設定することをサポートしています。(お好みで、#returnsを使うことができます)

mock(my_object).hello('bob', 'jane') { 'Hello Bob and Jane' }
mock(my_object).hello('bob', 'jane').returns('Hello Bob and Jane')  # returnsがついているが上と同じ

#times#at_least#at_most#any_timesメソッドでモックの期待する呼び出し回数を調整できます。#with_any_argsでどんな引数での呼び出しも許容したり、#with_no_argsで引数なしの呼び出しを期待したり、#neverでメソッドが呼ばれないことを期待したりできます。
もっと詳しい情報は、API overviewを参照ください。

WebMock

WebMockは、RubyでHTTPリクエストのスタブやモックを設定するためのgemです。
RRとの違いは、HTTPリクエストに特化している部分でしょうか。

GitHub:https://github.com/bblimke/webmock

Library for stubbing and setting expectations on HTTP requests in Ruby.

機能として、以下を提供しています。

  • HTTP リクエストを低レベルの http クライアントの lib レベルでスタブ化 (HTTP ライブラリを変更する際にテストを変更する必要はない)
  • HTTP リクエストに対する期待値の設定と検証
  • メソッド、URI、ヘッダ、ボディに基づいたリクエストのマッチング
  • 異なる表現における同じ URI のスマートマッチング (エンコードされた形式と非エンコードされた形式)
  • 異なる表現での同じヘッダのスマートなマッチング
  • Test::Unit、RSpec、minitest のサポート

WebMockの使い方

WebMockのGitHubページのサンプルから、使い方を抜粋します。

スタブ

stub_requestでリクエストをスタブすることができます。

uri のみに基づくスタブ付きリクエストとデフォルトのレスポンス

stub_request(:any, "www.example.com")    # スタブ(anyを使う)
Net::HTTP.get("www.example.com", "/")    # ===> Success

メソッド、URI、ボディ、ヘッダに基づいたスタブリクエスト

# スタブ
stub_request(:post, "www.example.com").
  with(body: "abc", headers: { 'Content-Length' => 3 })

uri = URI.parse("http://www.example.com/")
req = Net::HTTP::Post.new(uri.path)
req['Content-Length'] = 3
res = Net::HTTP.start(uri.host, uri.port) do |http|
  http.request(req, "abc")
end    # ===> Success

リクエストボディをハッシュと照合

ボディが、URL-Encode、JSON、XML のいずれかのとき、リクエストボディをハッシュと照合できます。

# スタブ
stub_request(:post, "www.example.com").
  with(body: {data: {a: '1', b: 'five'}})

RestClient.post('www.example.com', "data[a]=1&data[b]=five",
  content_type: 'application/x-www-form-urlencoded')    # ===> Success
RestClient.post('www.example.com', '{"data":{"a":"1","b":"five"}}',
  content_type: 'application/json')    # ===> Success
RestClient.post('www.example.com', '<data a="1" b="five" />',
  content_type: 'application/xml')    # ===> Success

hash_includingを使うと、部分的なハッシュとリクエストボディを照合できます。

# bodyをhash_includingで部分的なハッシュで照合
# bodyが全て一致していなくても照合できる
stub_request(:post, "www.example.com").
  with(body: hash_including({data: {a: '1', b: 'five'}}))

RestClient.post('www.example.com', "data[a]=1&data[b]=five&x=1",
:content_type => 'application/x-www-form-urlencoded')    # ===> Success

クエリパラメータの照合

ハッシュでクエリパラメータを照合できます。

# スタブ
stub_request(:get, "www.example.com").with(query: {"a" => ["b", "c"]})

RestClient.get("http://www.example.com/?a[]=b&a[]=c")    # ===> Success

ボディと同様、hash_includingで部分ハッシュとクエリパラメータと照合できます。

stub_request(:get, "www.example.com").
  with(query: hash_including({"a" => ["b", "c"]}))

RestClient.get("http://www.example.com/?a[]=b&a[]=c&x=1")    # ===> Success

hash_excludingを使うと、クエリパラメータ に含まれていない状態に照合できます。

stub_request(:get, "www.example.com").
  with(query: hash_excluding({"a" => "b"}))

RestClient.get("http://www.example.com/?a=b")    # ===> Failure
RestClient.get("http://www.example.com/?a=c")    # ===> Success

カスタムレスポンスを返すスタブ

to_returnでカスタムレスポンスを返すスタブを設定できます。

# スタブ
stub_request(:any, "www.example.com").
  to_return(body: "abc", status: 200,
    headers: { 'Content-Length' => 3 })

Net::HTTP.get("www.example.com", '/')    # ===> "abc"

エラーをraiseする

# クラスで宣言された例外のraise
stub_request(:any, 'www.example.net').to_raise(StandardError)
RestClient.post('www.example.net', 'abc')    # ===> StandardError

# 例外インスタンスのraise
stub_request(:any, 'www.example.net').to_raise(StandardError.new("some error"))

# 例外メッセージで例外をraise
stub_request(:any, 'www.example.net').to_raise("some error")

to_timeoutで、タイムアウト例外のraiseもできます。

stub_request(:any, 'www.example.net').to_timeout

RestClient.post('www.example.net', 'abc')    # ===> RestClient::RequestTimeout

繰り返すリクエストに複数の異なるレスポンス

リクエストが繰り返された時、複数の異なるレスポンスを返すことができます。
また、to_returnto_raiseto_timeoutthenでつないで複数のレスポンスを返したりtimesを使ってレスポンスを返す回数と指定することもできます。

stub_request(:get, "www.example.com").
  to_return({body: "abc"}, {body: "def"})
Net::HTTP.get('www.example.com', '/')    # ===> "abc\n"
Net::HTTP.get('www.example.com', '/')    # ===> "def\n"

# すべてのレスポンスが使用された後、最後のレスポンスが無限に返される
Net::HTTP.get('www.example.com', '/')    # ===> "def\n"

ネットワークへのリアルリクエストを許可または無効化

WebMock.allow_net_connect!で、実際のネットワークへのリクエストを許可できます。WebMock.disable_net_connect!で無効化することもできます。
特定のリクエストを許可しながら、外部リクエストを無効にすることもできます。

# 実際のネットワークへのリクエストを許可
WebMock.allow_net_connect!
stub_request(:any, "www.example.com").to_return(body: "abc")

Net::HTTP.get('www.example.com', '/')    # ===> "abc"
Net::HTTP.get('www.something.com', '/')    # ===> /.+Something.+/

# 実際のネットワークへのリクエストを無効化
WebMock.disable_net_connect!

Net::HTTP.get('www.something.com', '/')    # ===> Failure

他にも、様々な方法で、スタブすることができます。他の使い方のサンプルコードはStubbingページを参照ください。

期待値の設定(モック)

WebMockのGitHubページには、Test::UnitRSpecでの期待値の設定方法の記述はありましたが、minitestについて記述がありませんでした。
minitestは、Test::Unitと同様の書き方ができそうです(参考)。

Test::Unit/minitest

assert_requestedassert_not_requestedを使います。

require 'webmock/test_unit'

stub_request(:any, "www.example.com")

uri = URI.parse('http://www.example.com/')
req = Net::HTTP::Post.new(uri.path)
req['Content-Length'] = 3
res = Net::HTTP.start(uri.host, uri.port) do |http|
  http.request(req, 'abc')
end

assert_requested :post, "http://www.example.com",
  headers: {'Content-Length' => 3}, body: "abc",
  times: 1    # ===> Success

assert_not_requested :get, "http://www.something.com"    # ===> Success

assert_requested(:post, "http://www.example.com",
  times: 1) { |req| req.body == "abc" }

スタブを使って期待値を設定するためには、以下のように書きます。

stub_get = stub_request(:get, "www.example.com")
stub_post = stub_request(:post, "www.example.com")

Net::HTTP.get('www.example.com', '/')

assert_requested(stub_get)
assert_not_requested(stub_post)

Rspec

expecthave_requestedを組み合わせて書きます。

require 'webmock/rspec'

expect(WebMock).to have_requested(:get, "www.example.com").
  with(body: "abc", headers: {'Content-Length' => 3}).twice

expect(WebMock).not_to have_requested(:get, "www.something.com")

expect(WebMock).to have_requested(:post, "www.example.com").
  with { |req| req.body == "abc" }
# Note that the block with `do ... end` instead of curly brackets won't work!
# Why? See this comment https://github.com/bblimke/webmock/issues/174#issuecomment-34908908

expect(WebMock).to have_requested(:get, "www.example.com").
  with(query: {"a" => ["b", "c"]})

expect(WebMock).to have_requested(:get, "www.example.com").
  with(query: hash_including({"a" => ["b", "c"]}))

expect(WebMock).to have_requested(:get, "www.example.com").
  with(body: {"a" => ["b", "c"]},
    headers: {'Content-Type' => 'application/json'})

a_requesthave_been_madeを組み合わせて以下のようにも書けます。

expect(a_request(:post, "www.example.com").
  with(body: "abc", headers: {'Content-Length' => 3})).
  to have_been_made.once

expect(a_request(:post, "www.something.com")).to have_been_made.times(3)

expect(a_request(:post, "www.something.com")).to have_been_made.at_least_once

expect(a_request(:post, "www.something.com")).
  to have_been_made.at_least_times(3)

expect(a_request(:post, "www.something.com")).to have_been_made.at_most_twice

expect(a_request(:post, "www.something.com")).to have_been_made.at_most_times(3)

expect(a_request(:any, "www.example.com")).not_to have_been_made

expect(a_request(:post, "www.example.com").with { |req| req.body == "abc" }).
  to have_been_made

expect(a_request(:get, "www.example.com").with(query: {"a" => ["b", "c"]})).
  to have_been_made

expect(a_request(:get, "www.example.com").
  with(query: hash_including({"a" => ["b", "c"]}))).to have_been_made

expect(a_request(:post, "www.example.com").
  with(body: {"a" => ["b", "c"]},
    headers: {'Content-Type' => 'application/json'})).to have_been_made

スタブを使って期待値を設定するためには、以下のように書きます。

stub = stub_request(:get, "www.example.com")
# ... make requests ...
expect(stub).to have_been_requested

詳細は期待値の設定ページを参照ください。

その他

WebMock.reset!で現在のスタブとリクエストの履歴をすべてリセットしたり、WebMock.reset_executed_requests!で実行されたリクエストのカウンタのみをリセットできます。
WebMock.disable!WebMock.enable!で、WebMock を無効にしたり有効にしたり、一部の http クライアントアダプタのみを有効にすることができます。
他の機能については、WebMockのGitHubページのサンプルコードを参照ください。

MiniTest::Mock

最後に、MiniTest::Mockは、minitestに含まれているモックオブジェクトのフレームワークです。

公式ドキュメント:http://docs.seattlerb.org/minitest/Minitest/Mock.html

A simple and clean mock object framework.
All mock objects are an instance of Mock.
(シンプルでクリーンなモックオブジェクトフレームワークです。すべてのモックオブジェクトは MiniTest::Mockのインスタンスです。)

MiniTest::Mockの使い方

スタブ

オブジェクトをスタブするstubは、Minitest::Mock のオブジェクト拡張です。
スタブが有効なのはブロック内のみで、ブロックの最後にスタブはクリーンアップされます。また、スタブする前にメソッド名が存在している必要があります。
stub_any_instanceメソッドは、クラスのインスタンス上にメソッドスタブを作成できます。minitest-stub_any_instance_ofのgemを導入すると使うことができます。

  • stub:オブジェクトのメソッドをスタブする
  • stub_any_instance_of:クラスのインスタンスメソッドをスタブする

stubのサンプルコードです。

require 'minitest/autorun'

# スタブする対象のクラス
class Hello
  def say
    'Hello!'
  end
end

hello = Hello.new
# helloオブジェクトのsayメソッドが'Hello, this is from stub!'を返すようにスタブする
hello.stub(:say, 'Hello, this is from stub!') do
  hello.say  #==> "Hello, this is from stub!"
end
# ブロックを抜けるとスタブは無効になる
hello.say  #==> "Hello!"

stub_any_instanceを使うと、インスタンスメソッドのスタブを以下のように書けます。インスタンスメソッドのスタブを書くときはこちらの方が使える場面が多そうです。

require 'minitest/autorun'
require 'minitest/stub_any_instance'  # minitest-stub_any_instance_ofのgemも必要

# スタブする対象のクラス
class Hello
  def say
    'Hello!'
  end
end

# Helloクラスの任意のインスタンスのsayメソッドが'Hello, this is from stub!'を返すようにスタブする
Hello.stub_any_instance(:say, 'Hello, this is from stub!') do
  Hello.new.say  #==> "Hello, this is from stub!"
end
# ブロックを抜けるとスタブは無効になる
Hello.new.say  #==> "Hello!"

モック

expectメソッド

`expect(name, retval, args = [], &blk)
メソッド名(name)が呼ばれ、オプションで引数(args)またはブロック(blk)を指定し、戻り値(retval)を返すことを期待します。

require 'minitest/autorun'

@mock.expect(:meaning_of_life, 42)
@mock.meaning_of_life # => 42

@mock.expect(:do_something_with, true, [some_obj, true])
@mock.do_something_with(some_obj, true) # => true

@mock.expect(:do_something_else, true) do |a1, a2|
  a1 == "buggs" && a2 == :bunny
end

引数は、'==='演算子を使って期待される引数と比較されるので、より具体的な期待値が少なくて済むようになっています。(含まれるか?で比較される)

require 'minitest/autorun'

# users_any_stringメソッドがStringに含まれる場合、trueを返す
@mock.expect(:uses_any_string, true, [String])
@mock.uses_any_string("foo") # => true
@mock.verify  # => true(期待通りにモックが呼ばれたのでtrueになる)

@mock.expect(:uses_one_string, true, ["foo"])
@mock.uses_one_string("bar") # => raises MockExpectationError(期待通りにモックが呼ばれなかったため)

メソッドが複数回呼ばれる場合は、それぞれに新しい期待値を指定します。これらは定義した順番で使用されます。

require 'minitest/autorun'

@mock.expect(:ordinal_increment, 'first')
@mock.expect(:ordinal_increment, 'second')

@mock.ordinal_increment # => 'first'
@mock.ordinal_increment # => 'second'
@mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment"

verifyメソッド

すべてのメソッドが期待通りに呼び出されたことを確認します。期待通りに呼ばれたらtrueを返します。モックオブジェクトが期待通りに呼ばれなかった場合、MockExpectationErrorを発生させます。

詳しくは、MiniTest::Mockページを参照ください。

最後に

RRもWebMockも、公式ドキュメントに十分な使い方のサンプルが掲載されていたので、一読してみると良さそうです。MiniTest::Mockの情報量は少なめだったので、irbrails cmockstubの動きを確認してみると、想像がつきやすくなると思いました。(実行時に、require 'minitest/autorun'が必要です。)

参考情報

RR / GitHub
RRのページ
WebMock / GitHub
MiniTest::Mock
MiniTest stub
minitest-stub_any_instance
Mock、Stub勉強会(ruby)
自動テストのスタブ・スパイ・モックの違い
Test Double / xUnit Patterns.com
minitest で stub, mock を使う
wiki テストダブル

xUnit Test Pattern

テストダブルのバリエーションを調べていると、こちらのxUnit Test Patterns: Refactoring Test Codeの書籍がよく出てきました。
英語版しか出版されていないようですが、Webで内容を確認できました(英語です)。
http://xunitpatterns.com

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

【Rails】ancestryを用いた多階層カテゴリー機能の実装『Bootstrap3でウィンドウ作ってみた編』

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装
多対多のカテゴリー機能実装
多階層カテゴリー機能実装(準備編)
多階層カテゴリー機能実装(seed編)
多階層カテゴリー機能実装(作成フォーム編)
多階層カテゴリー機能実装(編集フォーム編)

1.コントローラーを編集

homes_controller.rb
# 追記
def category_window
  @children = Category.find(params[:parent_id]).children
end

【解説】

① Ajax通信で送られてきたパラメーターに対応するカテゴリーの、子カテゴリーを抽出し、インスタンス変数に代入する。

@children = Category.find(params[:parent_id]).children

2.json.jbuilderファイルを作成・編集

ターミナル
$ touch app/views/homes/category_window.json.jbuilder
category_window.json.jbuilder
json.array! @children do |children|
  json.id children.id
  json.name children.name
end

【解説】

get_category_childrenアクションで抽出したレコードを繰り返し処理し、配列を作成する。

json.array! @children do |children|

② 各IDと名前を で作成した配列に格納する。

json.id children.id
json.name children.name

◎ 親カテゴリー(ビジネス)にマウスが乗っている場合の返り値

[
  {
    "id": 2, 
    "name": "金融"
  },
  {
    "id": 6, 
    "name": "経済"
  },
  {
    "id": 9, 
    "name": "経営"
  },
  {
    "id": 13, 
    "name": "マーケティング"
  },
]

◎ 子カテゴリー(金融)にマウスが乗っている場合の返り値

[
  {
    "id": 3, 
    "name": "株"
  },
  {
    "id": 4, 
    "name": "為替"
  },
  {
    "id": 5, 
    "name": "税金"
  },
]

3.ルーティングを追加

routes.rb
# 追記
get 'get_category/new', to: 'homes#category_window', defaults: { format: 'json' }

4.ビューを編集

application.html.slim
body
  header
    nav.navbar.navbar-default.navbar-fixed-top
      .container-fluid
        ul.nav.navbar-nav.navbar-right
          li.dropdown role='presentation'
            a.dropdown-toggle data-toggle='dropdown' href='#' role='button' aria-expanded='false'
              i.fas.fa-list-ul
              span
                |  カテゴリーから探す
              span.caret
            ul.dropdown-menu role='menu'
              li role='presentation'
                - Category.where(ancestry: nil).each do |parent|
                  = link_to parent.name, root_path, id: "#{parent.id}", class: 'parent-category'
              br
              li role='presentation' class='children-list'
              br
              li role='presentation' class='grandchildren-list'

【解説】

※Bootstrapの書き方については省略します。

① ancestryの値がnil、つまり親カテゴリーを全て抽出し、プルダウンメニューに表示する。

- Category.where(ancestry: nil).each do |parent|
  = link_to parent.name, root_path, id: "#{parent.id}", class: 'parent-category'

② 子カテゴリーを表示する場所を用意する。

li role='presentation' class='children-list'

③ 孫カテゴリーを表示する場所を用意する。

li role='presentation' class='grandchildren-list'

5.JavaScriptファイルを作成・編集

ターミナル
$ touch app/assets/javascripts/category_window.js
category_window.js
$(function() {
  function buildChildHTML(children) {
    let html = `
      <a class="children-category" id="${children.id}" href="/">
        ${children.name}
      </a>
    `;
    return html;
  }

  $('.parent-category').on('mouseover', function() {
    let id = this.id;
    $('.children-category').remove();
    $('.grandchildren-category').remove();
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildChildHTML(child);
        $('.children-list').append(html);
      });
    });
  });

  function buildGrandChildHTML(children) {
    let html = `
      <a class="grandchildren-category" id="${children.id}" href="/">
        ${children.name}
      </a>
    `;
    return html;
  }

  $(document).on('mouseover', '.children-category', function() {
    let id = this.id;
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildGrandChildHTML(child);
        $('.grandchildren-list').append(html);
      });
      $(document).on('mouseover', '.children-category', function() {
        $('.grandchildren-category').remove();
      });
    });
  });
});

【解説】

① 子カテゴリーのHTMLを作成する。

function buildChildHTML(children) {
  let html = `
    <a class="children-category" id="${children.id}" href="/">
      ${children.name}
    </a>
  `;
  return html;
}

② どの親カテゴリーにマウスが乗っているかによって、子カテゴリーの表示内容を変更する。

  $('.parent-category').on('mouseover', function() {
    let id = this.id;
    $('.children-category').remove();
    $('.grandchildren-category').remove();
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildChildHTML(child);
        $('.children-list').append(html);
      });
    });
  });

◎ 親カテゴリーにマウスが乗った時に発火するイベントを作成する。

$('.parent-category').on('mouseover', function() {});

category_window.json.jbuilderから送られてきたIDを、変数へ代入する。

let id = this.id;

◎ とりあえず子カテゴリー以下を削除しておく。

$('.children-category').remove();
$('.grandchildren-category').remove();

◎ パラメーター(parent_id)に先ほど作成した変数を設定して、category_windowアクションを非同期で実行する。

  $.ajax({
    type: 'GET',
    url: '/get_category/new',
    data: {
      parent_id: id,
    },
    dataType: 'json',
  })

◎ Ajax通信が成功した場合は対応する子カテゴリーのHTMLを作成し、表示する。

.done(function(children) {
  children.forEach(function(child) {
    var html = buildChildHTML(child);
    $('.children-list').append(html);
  });
});

③孫カテゴリーのHTMLを作成する。

function buildGrandChildHTML(children) {
  var html = `
    <a class="grandchildren-category" id="${children.id}" href="/">
      ${children.name}
    </a>
  `;
  return html;
}

④ どの子カテゴリーにマウスが乗っているかによって、孫カテゴリーの表示内容を変更する。( とほぼ同じなので説明は省略)

$(document).on('mouseover', '.children-category', function() {
  var id = this.id;
  $.ajax({
    type: 'GET',
    url: '/get_category/new',
    data: {
      parent_id: id,
    },
    dataType: 'json',
  }).done(function(children) {
    children.forEach(function(child) {
      var html = buildGrandChildHTML(child);
      $('.grandchildren-list').append(html);
    });
    $(document).on('mouseover', '.children-category', function() {
      $('.grandchildren-category').remove();
    });
  });
});

注意

turbolinksを無効化しないとプルダウンメニューが非同期で動作しないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

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

【Rails】アプリ開発の手順からGit管理まで

はじめに

未経験からバックエンドエンジニアを目指す22卒予定大学生@million_momobanです。
Webアプリの開発を進めつつ、就職やインターンの情報収集も行っています。

22卒予定で私と同じようにエンジニア志望の方、是非繋がりましょう!

状況

Railsアプリ作成中にミスを犯してしまい、ただ1から作り直すくらいなら手順を記録して復習しながら作り直した方がやり直す理由にもなるし、同じ境遇の方の手助けにもなると考え今に至ります。

初学者である私がよくつまずく「環境構築」「Git管理」に重点を置いてアプリ開発の一連の流れを記録し共有します。

記事内容でも技術面でもそれ以外でもコメント大歓迎です!というかください!!

こんな方へ

  • Railsでアプリ開発したいけど腰が重い
  • Gitを使っているようで使いこなせていない
  • 疑似プルリクってよく聞くけど何なんだそれ

環境

  • Mac
  • Xcode / Homebrew / rbenv をインストール済み
  • VSCode / iTerm2
  • GitHubの登録・SSH接続済み

作業スペースの確保から

デスクトップからFinderを開いてUsers -> (ユーザー名)へと進み、今回はPortfolioという名前のディレクトリを作成しプロジェクトを管理します。もちろんコマンドラインから作成してもいいです。

ターミナルを開いてPortfolioまで移動、Rubyのバージョンを指定してインストールします。
2.6.5を指定している意味は特にないです。

/Users/hoge/Portfolio
$ rbenv install 2.6.5
・・・
Installed ruby-2.6.5 to /Users/hoge/.rbenv/versions/2.6.5

数分待ってこうなればオッケー?
他にもバージョンが存在していて、思った通りのバージョンにならなかったら$ rbenv global 2.6.5でバージョンをその環境全体に適用してあげる。

/Portfolio
$ bundle init
・・・
Writing new Gemfile to /Users/hoge/Portfolio/Gemfile

新しく生成されたPortfolio配下のGemfileをFinderから直接開く。中身が次のようになっていると思うので一番下の# gem "rails"gem "rails"にする。⌘+Sキーで上書き保存してファイルを閉じる。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails" ←ここを変更する

できたらターミナルに戻って次のコマンドでrailsをインストールする。

/Portfolio
$ bundle install --path vendor/bundle

オプションの---path vendor/bundleはあってもなくても良いそうです。
参考: bundle install時に--path vendor/bundleを付ける必要性は本当にあるのか、もう一度よく考えてみよう

Railsプロジェクトの開始

インストール出来るRailsを確認してみます。

/Portfolio
$ gem list rails

*** LOCAL GEMS ***

当然何もありません。
バージョンを指定してgemをインストールします。
5.2.4.2を指定しているのは以前の開発環境と同じにするため。

/Portfolio
$ gem install -v 5.2.4.2 rails
・・・
$ gem list rails

*** LOCAL GEMS ***

rails (5.2.4.2)
...

gemがインストールされたのでようやくプロジェクトを開始できそうです。

Portfolio
$ bundle exec rails _5.2.4.2_ new (プロジェクト名) -d mysql --skip-turbolinks --skip-test --skip-bundle
create
      create  README.md
      create  Rakefile
・・・

コマンドの解説は次の通り。場合によって追加したり削除したりしてください。

  • rails _ ○.○.○_
    • バージョンを指定してインストール
  • -d mysql
    • データベースを軽量で小規模向きであるデフォルトのSQLiteから高速で安定性のあるMySQLに変更。好みの問題だが最近ではこっちのほうがよく見かける。
  • --skip-turbolinks
    • ターボリンクをオフに。ページの遷移が早くなります。
  • --skip-test
    • minitestではなくRSpecを使うので。よくわからない人は後からでも変えられるので書かなくてもいいです。
  • --skip-bundle
    • この後手動でbundleするので追加

終わったらプロジェクト名のディレクトリの中にあるGemfileに必要なgemを記述していきます。
上の方の記述にある'= 5.2.4.2'ですが、このように=にしてあげることでこれ以上のバージョンにならないようにしてあげます。

Gemfile
・・・

ruby '2.6.5'

gem 'rails', '= 5.2.4.2'
gem 'mysql2', '>= 0.4.4', '< 0.6.0'

・・・

ターミナルから$ cd (プロジェクト名)でプロジェクトに移動し、$ bundle install

/portfolio/hoge
$ bundle install
・・・

An error occurred while installing mysql2 (0.5.3), and Bundler cannot continue.
Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'` succeeds before bundling.

なにやらmysql2でエラーが発生した模様。 
色々試してみましたがウェブ帳さんのRails5 gemでmysql2が インストールできないに記述してある

$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"

で解決しました。再度bundle install
動作確認でrails sをしlocalhost:3000にアクセスしてみる。

スクリーンショット 2020-06-20 13.25.58.png

接続もできていてバージョンもバッチリですね。
$ bundle exec rails db:createでデータベースも作成しておきます。

Git管理とGitHub

次にGit管理をしていきます。
ローカルリポジトリの管理にはSourcetreeやGitHub desktopなどがありますが、今回はVSCode上のもの+拡張機能で管理していきます。機能も初学者の個人開発程度であれば充分すぎますし、開くソフトも少なく済みます。
GitHistoryという拡張機能が便利なのでインストールしておきます。

やってみる

GitHubのRepositoriesのNewから新しくプロジェクトを開始します。
Repository nameには好きな名前を、DescriptionやREADMEについてはそのままでokです。
Public(公開)にしていたら情報が漏れたりと危険なのでPrivateにチェックを入れておきましょう。
スクリーンショット 2020-06-20 16.20.40.png

Create repositoryを押したら一旦VSCodeに移ります。
VSCodeを起動したら「フォルダを開く」から作成したプロジェクトを開きます。
ターミナルを開き、Create repositoryをした後に表示される5行のコマンドを順に入力していきます。

/Portfolio
$ git init
$ git add .
$ git commit -m "first commit"
$ git remote add origin https://github.com/******/sample.git
$ git push -u origin master

コマンドの説明としては次の通り。

  • git init
    • gitによる管理を開始
  • git add .
    • 編集したファイルをコミットするためのステージング環境に追加します。コミットをすることでゲームで言うセーブデータを一つ作るような工程を行うためその前準備みたいなものです。.(ドット) はカレントディレクトリ配下のすべてを意味し、この際では全てのファイルをコミットするための準備としてステージング環境に追加しています。
  • git commit -m "first commit"
    • ローカルリポジトリに"first commit"(最初のコミット)というメッセージを残してコミットをしています。-m "○○"で○○というメッセージを追加することを意味します。「最初の城を攻略したよ」みたいなメモと一緒にセーブするような感覚です。
  • git remote add origin https...
    • リモートのサーバ(https...)にoriginという短縮名をつけてあげているんです。毎回毎回https...を打っていたら面倒ですからね。originなのは慣習的にそう決まっているそう。
  • git push -u origin master
    • pushすることでコミットしたしたファイル等を複数人に共有できるリモートリポジトリにアップロードします。masterというブランチにローカルリポジトリの変更を反映させます。ちなみにただmasterだけだとローカルリポジトリのmasterブランチということになります。

これで準備は整いました。

いざ開発

VSCodeのGit Historyを見ていきましょう。画像の通り1、2とクリックすると履歴を見ることができ、最初に行ったfirst commitを確認することができます。
スクリーンショット 2020-06-20 17.08.13.png

3から現在masterというブランチにいることがわかります。ブランチ(枝)とは開発の環境を互いに影響を与えずに「修正用」とか「機能追加用」とか枝分かれさせていき柔軟に開発を進めるためのものです。

master と topic

デフォルトのmasterブランチには慣習的に安定版や完成品を置いておき、masterブランチ上で開発を行わない事が多いです。そのため新しく開発用のブランチを作成しましょう。3から新しくブランチを作成します。今回は'topic'と名前をつけました。

現在のGitの状態を見れるgit statusをターミナルで打ってみます。

/Portfolio/hoge
$ git status
On branch topic
nothing to commit, working tree clean

トピック(topic)ブランチにいるよ、コミットするものはないよ、作業スペースもキレイといっています。

addしてみる

開発用のtopicブランチにいることを確認したら少しファイルを弄ってみます。gitignoreを設定し直しREADME.mdも少し変えてみました。サイドバーのGitマークをクリックし中身を見てみると先程の2つのファイルが作業スペースにあるので、+ボタンをクリックしてステージング環境に移動させてやります。
これはターミナルでいう$ git addと同じことをしています。
スクリーンショット 2020-06-20 17.38.33.png

ステージング済みになりコミットする準備が整いましたね。そしたらその上のテキストボックスにコミット内容を記述し、更に上のチェックボタンを押して変更をコミットします。
これもまたターミナルでいう$ git commit -m "動作確認"と同等です。
スクリーンショット 2020-06-20 17.40.25.png

再び履歴を見てみます。次の写真に注目すると、最新の状態である上段に緑色のtopicブランチ、下段(変更前)に緑色のmasterブランチと赤色のorigin/masterブランチがありますね。topicブランチで開発を進めているわけですから時系列が一番新しいのも理解できると思います。

スクリーンショット 2020-06-20 18.19.14.png

さて、このまま変更をローカルのmasterブランチに反映させても問題なさそうなのでマージしたいと思います。まず左下のtopicを押してmasterブランチを選び、移動したこと確認します。
スクリーンショット 2020-06-20 18.16.49.png

緑色のtopicの真下にあるMoreをクリックしてMerge thisを選びます。
topicを選択し、最終確認を済ませると...
スクリーンショット 2020-06-20 17.55.26.png

きちんとmasterにもtopicで変更された内容が反映されました!!
スクリーンショット 2020-06-20 17.56.53.png

プッシュとプルリクエスト

実は先程のブランチを切り替える際に押したtopicやmasterが書いてあるボタンの右に、雲マークもしくは2本の矢印が丸を描いているボタンがあると思います。それをクリックすれば簡単にリモートリポジトリへ現在の変更を同期することができます。

でもこれ考えてみてください。チーム開発していたとして突然、完成品のみを扱うと決めたmasterブランチに他のメンバーから確認もなしに変更を同期されるようなものです。

プルリクエストを作れば、マージするに値するのかチームメンバーにコードをレビューしてもらい、許可が下りればマージされるみたいなことが出来事故も防げます。個人開発ではありますが慣れておいて損はないでしょう。

やり方

topicブランチで開発を進め、3つのファイルの変更があったとします。+ボタンでステージングにaddをしてコミットメッセージを適当につけます。
スクリーンショット 2020-06-20 19.00.11.png

コミットもして、先程の通りmasterブランチに切り替えてtopicブランチをマージします。
スクリーンショット 2020-06-20 19.02.18.png
うん、予想通り。
ここからリモートリポジトリにプッシュをしていくのですが流れとしては次の通りです。

ローカルリポジトリのmasterブランチからリモートリポジトリのorigin/topicブランチにプッシュする(正直topic → origin/topicでいい気がする)

GitHub上でリモートリポジトリのorigin/topicブランチからorigin/masterへのプルリクエストを作る

リポジトリの管理者にマージしてもらう。

先程の続きからやっていきましょう。orign/topicへプッシュします。
あれ、リモートリポジトリにはまだmasterブランチしかないよ?と思った方は安心してください。ブランチがない場合は状況に応じて新しくブランチを作ってくれます。

/Portfolio/○○
$ git push origin topic

・・・
remote: Create a pull request for 'topic' on GitHub by visiting:
remote:      https://github.com/******/sample/pull/new/topic
remote:
To https://github.com/******/sample.git
 * [new branch]      topic -> topic

GitHub上でtopicへのプルリクエスト作ります。
topic -> topic は topic -> origin/topic のことですね。

疑似チーム開発

次にGitHubを覗いてみます。プロジェクトのトップページへ行くと'topic' Compare & pull request とありますね。ここを押してプルリクを作成します。
スクリーンショット 2020-06-21 10.03.07.png

上の枠には、topicからmasterへのプルリクを比較していてそれがマージ可能であることを示しています。
確認できたらタイトルとメッセージを書きます。タイトルにはコミットした際のメッセージが最初から当てられていると思います。
スクリーンショット 2020-06-21 10.08.16.png

メッセージは自分以外の人が見ても理解できる内容を書くようにしましょう。
プルリクの書き方についてはこちらの方の記事がとても参考になりました。
GitHub「完璧なプルリクの書き方を教えるぜ」

メッセージも書けたら'Create pull request' します。
今回は個人開発でリポジトリの管理者も自分だけな為すぐにマージできますが、せっかくなので色々触ってみます。タブからFile changedをクリックします。
スクリーンショット 2020-06-21 10.32.41.png

ここでは変更のあったファイルをレビューすることが出来ます。疑問や提案、称賛のコメントを積極的に残しましょう。
一通り書いたら右上部の'Review changes'から'Submit review'でレビューをします。
スクリーンショット 2020-06-21 10.30.40.png

'Conversion'タブへ戻るとレビュー等が反映されています。
スクリーンショット 2020-06-21 10.42.34.png

最終確認を済ませたら'Merge pull request'から'Confirm merge'でマージを完了。
スクリーンショット 2020-06-21 10.46.02.png

トップページへ戻ってみると先程のマージが反映されてますね!
スクリーンショット 2020-06-21 10.51.24.png

これでやっと一連の流れが完了です。
このあたかも複数人で開発しているかのように見せる行為が疑似プルリクと言われる所以だと思います。

まとめ

この記事を書いてみて、理解しているようで出来ていなかったことを沢山知ることが出来ました。それでも書いていて怪しいなと思うことも多く(特にブランチの運用あたり)、まだまだ勉強が必要だなと思いました。

初めてこんなにも長い文を書いたのでおかしな部分もあると思います!
これからも現在進行系で開発を進めながら記録を書き残しています。
プロジェクトが一段落ついたり、完成したりすればまた書こうと思っています。次はRSpecかな??

地方在住でも独学でも情報系でなくても周りにプログラミングをやっている友達がいなくてもそれなりの成果物を完成させてバックエンドエンジニアとして就職が出来ることを証明して見せます!!

学生として就活情報やインターンなど経験も記事にして発信していくつもりです。
記事のことでも記事以外のことでもどんな内容でもいいのでコメントお待ちしています!!

参考記事

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

ancestryを用いた多階層カテゴリー機能の実装『編集フォーム編』

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装
多対多のカテゴリー機能実装
多階層カテゴリー機能実装(準備編)
多階層カテゴリー機能実装(seed編)
多階層カテゴリー機能実装(作成フォーム編)

実装

1.コントローラーを編集

books_controller.rb
def edit
  unless @book.user == current_user
    redirect_to books_path
  end
  @category_parent_array = Category.category_parent_array_create
end

def update
  if @book.update(book_params)
    book_categories = BookCategory.where(book_id: @book.id)
    book_categories.destroy_all
    BookCategory.maltilevel_category_create(
      @book,
      params[:parent_id],
      params[:children_id],
      params[:grandchildren_id]
    )
    redirect_to @book
  else
    @category_parent_array = Category.category_parent_array_create
    render 'edit'
  end
end

【解説】

① 中間テーブルから編集する本に対応するレコードを全て抽出し、削除する。

book_categories = BookCategory.where(book_id: @book.id)
book_categories.destroy_all

2.ビューを編集

books/edit.html.slim
/ 追記
.category-form
  = label_tag 'ジャンル'
  = select_tag 'parent_id', options_for_select(@category_parent_array), class: 'form-control', id: 'parent-category'
  i.fas.fa-chevron-down
br

注意

turbolinksを無効化しないとセレクトボックスが非同期で動作しないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

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

【Rails】ancestryを用いた多階層カテゴリー機能の実装『編集フォーム編』

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装
多対多のカテゴリー機能実装
多階層カテゴリー機能実装(準備編)
多階層カテゴリー機能実装(seed編)
多階層カテゴリー機能実装(作成フォーム編)

実装

1.コントローラーを編集

books_controller.rb
def edit
  unless @book.user == current_user
    redirect_to books_path
  end
  @category_parent_array = Category.category_parent_array_create
end

def update
  if @book.update(book_params)
    book_categories = BookCategory.where(book_id: @book.id)
    book_categories.destroy_all
    BookCategory.maltilevel_category_create(
      @book,
      params[:parent_id],
      params[:children_id],
      params[:grandchildren_id]
    )
    redirect_to @book
  else
    @category_parent_array = Category.category_parent_array_create
    render 'edit'
  end
end

【解説】

① 中間テーブルから編集する本に対応するレコードを全て抽出し、削除する。

book_categories = BookCategory.where(book_id: @book.id)
book_categories.destroy_all

2.ビューを編集

books/edit.html.slim
/ 追記
.category-form
  = label_tag 'ジャンル'
  = select_tag 'parent_id', options_for_select(@category_parent_array), class: 'form-control', id: 'parent-category'
  i.fas.fa-chevron-down
br

注意

turbolinksを無効化しないとセレクトボックスが非同期で動作しないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

続編

多階層カテゴリー機能実装(Bootstrapeでウィンドウ作ってみた編)

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

Rails プレゼンターを導入する

プレゼンターを導入する

プレゼンターとはView部分にあるロジックのHTMLコードを精製する役割を担います。
要はview部分をスッキリさせようぜってもんです。
プレゼンターはデコレーターとも呼ばれます。

GemではDraper,Cellsなどが使われているようですが、
Gemを今回は使わずに実装をしていきます。

helperはあかんのか?

ビューで使用するメソッドなのでヘルパーメソッドとして定義するのは自然です。
でもhelperはグローバルに定義される、メリットとデメリットがあります。
プロジェクトが大きくなるにつれて名前が衝突するリスクが増してしまいます。

今回は以下のコードに含まれるロジック部分を分離していきます。
犯罪者リストのロジックです。
arrested?(逮捕されているか?)でtrueの場合は☑️をつけ、
そうでない場合は空欄の□となります。

<% @members.each do |m| %>
  <%= m.arrested? ? raw("&#x2611;") : raw("&#x2610;") %>

Modelに関するプレゼンターを作る

まずすべてのプレゼンターの祖先となるModelPresenterクラスを作ります。
呼び出し専用のobject属性とview_context属性が定義されています。

app/presenters/model_presenter.rb
class ModelPresenter
  attr_reader :object, :view_context
  delegate :raw, to: :view_context

  def initialize(object, view_context)
    @object = object
    @view_context = view_context
  end
end

次にmodel_presenterクラスを継承してMemberプレゼンタークラスを作ります。

member_presenter.rb
class StaffMemberPresenter < ModelPresenter
end

ERBテンプレートを編集

このクラスを用いてERBテンプレートを編集します。
MemberPresenterクラスのインスタンスを生成します。

newメソッドの1つ目の引数にはMemberオブジェクト。
2つ目の引数には疑似変数selfを指定しています。
selfではRailsで定義されているすべてのヘルパーメソッドを利用できます。

<% @members.each do |m| %>
  <% p = MemberPresenter.new(m, self) %>
    <%= m.arrested? ? raw("&#x2611;") : raw("&#x2610;") %>

Presenterをにメソッドを定義する

ここで先ほど作ったMemberPresenterクラスにインスタンスメソッドを定義していきます。

member_presenter.rb
class MemberPresenter < ModelPresenter
  def arrested_mark
    object.arrested? ? 
      view_context.raw("&#x2611") : 
      view_context.raw("&#x2610")
  end
end

ERBテンプレートを書き換える

今までのプレゼンターを利用してview部分をスッキリとさせます

<% @members.each do |m| %>
  <% p = MemberPresenter.new(m, self) %>
    <%= p.arrested_mark %>

変更箇所には以下のようなコードが埋め込まれています。

m.arrested? ? raw("&#x2611;") : raw("&#x2610;")

こんな感じでviewをスッキリさせることができました。
delegateを用いるともう少し簡潔にかけるのでその記事はまた書きたいと思います。
本日は以上です。

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

irbとpryの違いについて

irbはRuby付属の対話的環境。標準搭載。

pryはgem。Railsのデバックにはpryがオススメらしい。

イメージとしては、irbよりpryの方が少しリッチ。(そんなに変わらないが)

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

Rails Tutorial 第6版 学習まとめ 第9章

概要

この記事は私の知識をより確実なものにするためにRailsチュートリアル解説記事を書くことで理解を深め
勉強の一環としています。稀にとんでもない内容や間違えた内容が書いてあるかもしれませんので
ご了承ください。
できればそれとなく教えてくれますと幸いです・・・

出典
Railsチュートリアル第6版

この章でやること

・ユーザーの任意でログイン情報を記憶しておき、ブラウザを再起動してもログインできる機能を追加する。

Remember me機能

↑でも述べた通りブラウザを閉じてもログインを保持する機能を実装する(Remember me)
トピックブランチを作成して作業を始める。

記憶トークンと暗号化

これからの作業や作成物がなかなか難しいので先回り知識を確認する。

・トークンとは
コンピュータが使うパスワードのようなもの。
パスワードは人間が作成して人間が管理するがトークンはコンピュータが作成してコンピュータが管理する。

・永続的cookiesと一時セッションについて
前章で作成した一時セッションはsessionメソッドを使って、cookiesにブラウザ終了時が有効期限のセッションを作成した。
今回はcookiesメソッドを使って期限が無限(正確には20年ほど)のセッションを作成する。
cookiesメソッドではsessionメソッドと違い情報が保護されないかつ、セッションハイジャックと呼ばれる攻撃の的になるため
ユーザーIDと記憶トークンをセットでcookiesに保存し、ハッシュ化したトークンをDBに保存することで
セキュリティを確保する。

・具体的にどういう処理で実装するのか
1. cookiesメソッドを使って暗号化したユーザーIDと記憶トークンをブラウザに保存
2. DBにはハッシュ化した記憶トークン(記憶ダイジェスト)を同時に保存しておく。
3. 次回アクセス時はブラウザに保存されている期限付きcookiesのトークンとDBに保存された記憶ダイジェストを比較して
ログイン処理を自動で行う。

大まかに内容を確認したので
さっそくDBに記憶ダイジェスト(remember_digest)を追加する。
rails g migration add_remember_digest_to_users remember_digest:string
以前説明した通りファイル名末尾にto_usersとつけることでusersテーブルにカラムを追加すると勝手に認識してくれる。

remember_digestはユーザーが読み出せる内容ではないのでインデックスを追加する必要もない。
そのため、このままマイグレートする。

記憶トークンを作成するにあたり、何を使うかだが
長くてランダムな文字列が好ましい。
SecureRandomモジュールのurlsafe_base64メソッドが用途的にマッチしているのでこれを使っていく。
このメソッドは64種の文字を用いて、長さ22のランダム文字列を返すメソッド。
記憶トークンはこのメソッドを使って自動生成することにする。

>> SecureRandom.urlsafe_base64
=> "Rr2i4cNWOwhtDeVA4bnT2g"
>> SecureRandom.urlsafe_base64
=> "pQ86_IsKILLv4AxAnx9iHA"

パスワードと同じくトークンはほかのユーザ⁻と重複しても問題ないが、一意なものを使うことで
ユーザーIDとトークンの両方が奪われでもしない限りはセッションハイジャックなどにもつながらない。

新規でトークンを作成する(生成する)メソッドをuserモデルに定義していく。

  def User.new_token
    SecureRandom.urlsafe_base64
  end

このメソッドもユーザオブジェクトは不要のためクラスメソッドとして定義する。

つぎにrememberメソッドを作成していく。
このメソッドではDBにトークンに対応した記憶ダイジェストを保存する。
DBにremember_digestは存在するがremember_tokenは存在しない。
DBに保存したいのはダイジェストのみだがユーザーオブジェクトに紐づいたトークンに対するダイジェストを保存したいので
トークン属性にもアクセスしたい。
つまりパスワードの時と同じく仮想の属性としてトークンが必要になる。
パスワード実装時はhas_secure_passwordが自動生成してくれたが、今回は
attr_accessorを使ってremember_tokenを作成する。

user.rb
class User < ApplicationRecord
  attr_accessor :remember_token

  # before_save { self.email.downcase! }
  # has_secure_password
  # VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  # validates :name, presence: true, length:{maximum: 50}
  # validates :email, presence: true, length:{maximum: 255},
  #                   format: {with: VALID_EMAIL_REGEX},uniqueness: true
  # validates :password, presence: true, length:{minimum: 6}

  # def User.digest(string)
  #   cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
  #                                                 BCrypt::Engine::cost
  #   BCrypt::Password.create(string, cost: cost)
  # end

  # def User.new_token
  #   SecureRandom.urlsafe_base64
  # end

  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest,User.digest(remember_token))
  end
end

rememberメソッドの1行目の
self.remember_token = User.new_token
selfを書かないとremember_tokenというローカル変数が作成されてしまうためここでは必須。

ここではパスワードにアクセスできないためupdate_attributeはバリデーションを素通りさせるために使っている。

演習

1.しっかり動く。
remember_tokenは22文字のランダム生成文字列
remember_digestはそれらのハッシュ化文字列になっていることが見てわかる。

>> user.remember
   (0.1ms)  begin transaction
  User Update (2.4ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2020-06-17 14:30:27.202627"], ["remember_digest", "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"], ["id", 1]]
   (6.1ms)  commit transaction
=> true
>> user.remember_token
=> "lZaXgeF42y5XeP-EEPzstw"
>> user.remember_digest
=> "$2a$12$u8cnBDCQX85e9gBHbQWWNeJq.mxKq8hhpt/FSYMtm2rI2ljWIhRLi"
>> 

2.どちらも動作は同じ
class << selfを使うとendの間まではすべてクラスメソッドとして定義される。
ここでのselfキーワードはインスタンスオブジェクトではなくUserクラスそのものを表しているため認識違いに注意。

ログイン状態の保持

永続cookiesに保存するためにはcookiesメソッドを使う。
sessionとおなじくハッシュとして使える。

cookiesはvalue(値)とexpires(有効期限)を持っていて

cookies[:remember_token] =  { value: remember_token, expires: 20.years.from_now.utc }

とすることでcookies[:remember_token]に有効期限が20年のremember_tokenの値を保存できる。
また有効期限が20年というのはよく使われるのでRailsには専用メソッドが追加されていて

cookies.permanent[:remember_token] = remember_token

としても同じ効果になる。

またユーザーIDも永続cookiesに保存するがそのまま保存するとIDがそのまま保存されてしまい、
cookiesがどのような形式で保存されているのか、攻撃者にバレバレになってしまうため、
暗号化する。
暗号化には署名付きcookieを使う。

cookies.signed[:user_id] = user.id
これで安全に暗号化して保存できる。

もちろんユーザーIDも永続cookiesとして保存する必要があるのでpermanentメソッドをつないで使う。
cookies.permanent.signed[:user_id] = user.id

このようにユーザーIDと記憶トークンをセットでcookiesに入れることで
ユーザーがログアウトするとログインできなくなる(DBのダイジェストが削除されるため)

最後にブラウザに保存されたトークンとDBのダイジェストを比較する方法だが
secure_passwordのソースコードを一部パクり

BCrypt::Password.new(remember_digest) == remember_token
このようなコードを使う。
このコードだとremember_digestとremember_tokenを直接比較している。
実は、Bcryptで==演算子が再定義されており、このコードは
BCrypt::Password.new(remember_digest).is_password?(remember_token)
という動作をしている。
これを利用して記憶ダイジェストと記憶トークンを比較するauthenticated?メソッドを定義する。

  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

ここでのremember_digestはself.remember_digestと同じである。
DBの記憶ダイジェストと引数に渡した記憶トークンを比較して正しければtrueを返す

さっそくsessions_controllerのログイン処理部にremember処理を追加する。

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user&.authenticate(params[:session][:password])
      log_in(user)
      remember user
      redirect_to user
    else
      flash.now[:danger] = "Invalid email/password combination"
      render 'new'
    end
  end

ここではrememberヘルパーメソッドを使う。(まだ定義していない)

↓rememberヘルパーメソッド
rb:sessions_helper.rb
def remember(user)
user.remember
cookies.signed.permanent[:user_id] = user.id
cookies.permanent[:remember_token] = user.remember_token
end

わかりづらいので補足。
Userモデルに定義したrememberメソッドで
user.rb
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest,User.digest(remember_token))
end

ユーザーオブジェクトに対して記憶トークンと記憶ダイジェストを生成する。

sessions_helperに定義したrememberメソッドで
1.Userモデルのrememberメソッドを呼び出し、トークンとダイジェストを生成。
2.cookiesにユーザーIDを暗号化して保存
3.cookiesに1で生成したトークンを保存

の流れ。
メソッド名が被っているので注意。

これでユーザー情報をcookiesに安全に保存できるようになったがログイン状態を見て
動的にレイアウトを変更するために使っていたcurrent_userメソッドが一時セッションにしか
対応していないため修正する。

  def current_user #現在ログイン中のユーザ⁻オブジェクトを返す
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user &. authenticated?(cookies[remember_token])
        log_in user
        @current_user = user
      end
    end
  end

・user_idというローカル変数を使うことでコードの重複を減らしている。
・ブラウザを開いた初回実行時は永続cookiesの処理が行われ、同時にログイン処理も行われるため
ブラウザを閉じるまでは@current_userにユーザーが保存されている。

現時点だとログアウト処理(永続cookies)を削除する方法がないため
ログアウトできない。
(すでにあるログアウトアクションだと一時セッションを削除するだけなので、永続cookiesから情報を取り出して
自動でログインしてしまうためログアウトができない。)

演習

1.ある。
image.png

2.動く。

>> user = User.first
   (1.1ms)  SELECT sqlite_version(*)
  User Load (0.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-18 15:18:53", password_digest: [FILTERED], remember_digest: "$2a$12$tAZFCVr39lkPONLS4/7zneYgOE5pcYDM2kX6F1yKew2...">
>> user.remember
   (0.1ms)  begin transaction
  User Update (2.8ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2020-06-18 15:23:21.357804"], ["remember_digest", "$2a$12$h3K3aZSBmXB7wGkNdsBrS.2/UaawMQ199DGMvTDU8upvvOKCzbeba"], ["id", 1]]
   (10.3ms)  commit transaction
=> true
>> user.authenticated?(user.remember_token)
=> true

ユーザーを忘れる

現在、永続cookiesを削除していないため、ログアウトできない。
この問題を解決するためにforgetメソッドを定義する。
このメソッドで記憶ダイジェストをnilにする。
さらにsessions_helperにもforgetメソッドを定義することで
こちらではcookiesに保存されたユーザーIDと記憶トークンも削除する。

  def forget(user) #永続セッションを削除・記憶ダイジェストもリセット
    user.forget
    cookies.delete[:user_id]
    cookies.delete[:remember_token]
  end
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end

一応ログアウト処理の流れをサラッとおさらい。
1. ユーザオブジェクトに保存された記憶ダイジェストをnilにする(Userモデルのforgetメソッド)
2. cookiesのユーザーIDと記憶トークンを削除(sessions_helperのforgetメソッド)
3. 一時セッションのユーザIDを削除
4. カレントユーザ(現在ログイン中のユーザー)をnilにする。

演習

1.削除されている。(実行画面は省略)なおChromeだと以前と同じく一時セッションが残ってしまっているがアプリの動作上は
問題ない。

2つの目立たないバグ

現時点で二つのバグが残っている。かなり面倒なので詳細に説明していく。

1つ目のバグ
複数のタブでログインしていて、タブ1でログアウトした後、タブ2でもログアウトした時。
タブ1内でlog_outメソッドを使ってログアウトした後だとcurrent_userがnilになっている。
この状態でもう一度ログアウトしようとすると削除するcookieが見つからないため失敗する。

2つ目のバグ
別ブラウザで(Chrome、Firefoxなど)ログインしている時。
1. Firefoxでログアウトするとremember_digestがnilになる。
2. Chromeを閉じると一時セッションは削除されるがcookiesは残るため、ユーザーIDからユーザーを見つけることができてしまう。
3. user.authenticated?メソッドで比較するremember_digestがFirefox側で既に削除されているため
比較対象がなくなりエラーが発生する。

このバグを修正するためにまずはバグをキャッチするテストを書き
それを修正するコードを書く。

delete logout_path
これをログインテストのログアウト処理後にもう一度挿入することで2回ログアウトを再現する。

このテストをパスさせるためには
ログイン中だけログアウト処理を行うようにすればいい。

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end

2つ目のバグについて、テストで異なるブラウザ環境を再現するのは難しいため、
Userモデルのremember_digestに関してのテストにとどめる。
具体的にはremember_digestがnilの時にはfalseを返すことをテストする。

  test "authenticated? should return false for a user with nil digest" do 
    assert_not @user.authenticated?('')
  end

テストをパスさせるためにauthenticated?メソッドを改良する
rb
def authenticated?(remember_token)
return false if remember_digest.nil?
BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

remember_digestがnilの場合には即座にreturnキーワードでfalseを返し処理を終了させる。

これで2つのバグが修正される。

演習

1.エラーが発生する。(実行画面は省略。)
2.これもエラーが発生する(EdgeとChrome)
3.確認済み。

[Remember me]チェックボックス

次はRememberme機能には欠かせない、チェックボックスを実装する(チェックした時だけ記憶する機能)

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

ラベルの内側に配置する理由に関してはhttps://html-coding.co.jp/annex/dictionary/html/label/
このサイトがわかりやすい
つまりラベルに指定されている者のどこをクリックしてもチェックボックスを押したのと同じ動作にできる。

CSSで形を整えたら準備完了。
チェックボックスでparams[:session][:remember_me]に1 or 0が入るようになったので
1の時に記憶するようにすればいい。

三項演算子を使って実装すると

  params[:session][:remember_me] == '1' ? remember(user) : forget(user)

remember userの行をこれに差し替えるだけ
ちなみに三項演算子は

  条件文 ? trueの時の処理 : falseの時の処理

という形式で書ける。
ちなみにparamsの数値はすべて文字列で記録されているため条件文の1は''で囲わないと
必ずfalse分が実行されてrememberできなくなるので注意

演習

1.↑でも注意書きを書いたがparamsの条件は'1'としないとうまくいかない。うまくいけばcookiesに値が保存されて
うまく動く。

2.

>> hungry = true
=> true
>> hungry ? puts("I'm hungry now") : puts("I'm not hungry now")
I'm hungry now
=> nil

[Remember me]のテスト

Remembermeが実装できたのでテストも作成していく。

[Remember me]ボックスをテストする

直前の三項演算子で実装したparams[:session][:remember_me] == '1' ? remember(user) : forget(user)
という部分はプログラムを触っている人だと1(真)0(偽)ということで
params[:session][:remember_me] ? remember(user) : forget(user)
と書きたくなるが、チェックボックスはあくまで1と0を返す。
Rubyでは1と0は真偽値ではなくどちらもtrueとして扱われるためこのように書くのは間違いになる。
このようなミスをキャッチできるテストを書かなければならない。

ユーザーを記憶するためにはログインが必要になる。今までは逐一postメソッドを使ってparamsハッシュを送っていたが
毎回やるのはさすがに手間なのでログイン用のメソッドを定義する。
log_inメソッドとの混乱を防ぐためにlog_in_asメソッドとして定義する。

test_helper
class ActiveSupport::TestCase
  # Run tests in parallel with specified workers
  parallelize(workers: :number_of_processors)

  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all
  include ApplicationHelper
  # Add more helper methods to be used by all tests here...
  def is_logged_in?
    !session[:user_id].nil?
  end

  def log_in_as(user)
    session[:user_id] = user.id
  end


end

class ActionDispatch::IntegrationTest
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params:{ session: { email: user.email,
                                                password: password,
                                                remember_me: remember_me}}
  end
end

log_in_asメソッドをActionDispatch::IntegrationTestとActiveSupport::TestCaseで2回別々に定義しているのは
統合テストではsessionメソッドを使えないから。
そのため統合テストでは代わりにpostリクエストを使ってログインしている。

どちらのテストも同じ名前にすることで統合テストでも単体テストでもログインしたい時には何も気にせずlog_in_asメソッド
を呼べばいい。

log_in_asメソッドを定義したのでRemember_meのテストを実装する。

  test "login with remembering" do 
    log_in_as(@user, remember_me: '1')
    assert_not_empty cookies[:remember_token]
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '1')
    delete logout_path
    log_in_as(@user, remember_me: '0')
    assert_empty cookies[:remember_token]
  end

log_in_as(@user, remember_me:'1')
デフォルト値を設定しているため本来不要だが比較しやすいようremember_me属性も入力している。

演習

1.↑の統合テストでは仮想属性remember_tokenにアクセスできないためcookiesが空でないことだけをテストしていたが
assignsメソッドを使うことで直前にアクセスしたアクションのインスタンス変数を取得できる。
上のテストの例ではlog_in_asメソッド内でsessions_controllerのcreateアクションにアクセスしているため
createアクションで定義されたインスタンス変数の値をシンボルを使って読みだすことができる。
具体的には
現在createアクションで使われているのはuserというローカル変数なのでこれに@をつけて@userという
インスタンス変数に変えてしまうことでassignsメソッドが読み出せるようになる。
あとはテストでassigns(:user)とすることで@userを読み出せる。

users_login_test.rb
  test "login with remembering" do 
    log_in_as(@user, remember_me: '1')
    assert_equal cookies[:remember_token] , assigns(:user).remember_token
  end
sessions_controller.rb
  def create
    @user = User.find_by(email: params[:session][:email].downcase)
    if @user&.authenticate(params[:session][:password])
      log_in(@user)
      params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
      redirect_to @user
    else
      flash.now[:danger] = "Invalid email/password combination"
      render 'new'
    end
  end

[Remember me]をテストする

sessions_helperにログイン処理やセッション関連のヘルパーメソッドを実装してきたが
current_userメソッドの分岐処理に関してテストが行われていない。
その証拠に何も関連性のない適当な文字列を代入してもテストがパスしてしまう。

GREENのテスト↓

sessions_helper.rb
  def current_user #現在ログイン中のユーザ⁻オブジェクトを返す
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      テストしてないから日本語も許される。
      user = User.find_by(id: user_id)
      if user &.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end

これはまずいのでsessions_helperようのテストファイルを作成する。

sessions_helper_test.rb
require 'test_helper'
class SessionsHelperTest < ActionView::TestCase
  def setup
    @user = users(:michael)
    remember(@user)
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    assert is_logged_in?
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    assert_nil current_user
  end
end

1つ目のテストでは記憶したユーザとcurrent_userが同じかどうか確かめ、ログインしているかも確かめている。
こうすることでテストがcookiesにユーザーIDが存在した際に中身の処理が動いているか確認できる。

2つ目のテストではremember_digestを書き換えることでrememberメソッドで記録したremember_tokenと対応させない
ようにした際にcurrent_userが期待通りnilを返す、つまりauthenticated?メソッドが
正しく動作しているかテストしている。

また、補足としてassert_equalメソッドは第1引数と第2引数を入れ替えても動作するが
書き方は第1引数に期待値、第2引数に実際の値と書かなければならないことに注意
このように書かなければエラーが発生した際にログの表示がかみ合わなくなってしまう。

そしてこの段階ではもちろんテストは通らない。

入れておいた全く関係のない文を削除することでテストがパスする。
これでcurrent_userのどの分岐もテストできるようになったため回帰バグもキャッチできる。

演習

1.記憶トークンと記憶ダイジェストが正しく対応していなくともuserが存在するだけでif文を通過してしまうため
返り値がnilでなくなってしまう。つまりテストも失敗する。

 FAIL["test_current_user_returns_nil_when_remember_digest_is_wrong", #<Minitest::Reporters::Suite:0x000055b13fd67928 @name="SessionsHelperTest">, 1.4066297989993473]
 test_current_user_returns_nil_when_remember_digest_is_wrong#SessionsHelperTest (1.41s)
        Expected #<User id: 762146111, name: "Michael Example", email: "michael@example.com", created_at: "2020-06-20 15:38:57", updated_at: "2020-06-20 15:38:58", password_digest: [FILTERED], remember_digest: "$2a$04$uoeG1eJEySynSb.wI.vyOewe9s9TJsSoI9vtXNYJxrv..."> to be nil.
        test/helpers/sessions_helper_test.rb:15:in `block in <class:SessionsHelperTest>'

↑current_userの返り値がnilになることが期待されているのに対し、userオブジェクトが返ってしまっていることを
エラーとして出力している。

前の章へ

次の章へ

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

progateで習得したrailsの基礎知識まとめ

どんな人向けの記事か

  • Railsを学び始めて間もない人
  • Railsで初めてアプリを作ろうとしている人
  • Rails慣れはしているけど少し復習したい人

どんな記事か

Rails初学者である筆者が、先日修了したprogateのRailsコースで学んだことを並べていく記事です。
ところどころ他記事の情報で補完しているため、progate運営が表現・説明した内容とは異なる箇所もそれなりにあります。

各種概念

サーバー

クライアント(ブラウザ)の要求に応じてデータを提供するコンピュータ。
Railsで作ったアプリをブラウザで表示するためには、このサーバーを起動させる必要がある。

ビュー

ページの見た目を作るためのHTMLファイル。ブラウザとRailsのやりとりの中で、Railsからビューが返されることで、ページが表示される。
拡張子は.html.erb。erbはEmbedded Rubyの略で、HTMLファイルにRubyスクリプトを埋め込むための仕組み。
下記のコントローラ・ルーティングと同様に、ページを作成するのに必要な3つの要素のうちの1つ。

コントローラ

ブラウザにビューを返すためのもの。コントローラは目的に合わせてそれぞれ作成される。
例えば投稿に関する機能とユーザーに関する機能はそれぞれ別のコントローラファイルで規定される。

アクション

コントローラ内の、ブラウザに返すビューを見つけ出すための機能。コントローラファイル内でメソッドとして表現される。

ルーティング

送信されたURLに対して、「どのコントローラ」の「どのアクション」で処理するかを決める、対応表のようなもの。

データベース

データを保存しておくための場所。

テーブル

データベース内でデータを管理している表。行ごとのデータをレコード、列ごとのデータをカラムと呼ぶ。

モデル

データベースとのやりとりを行うクラス。モデルのインスタンスを作り、それを保存する工程を経ることでデータをテーブルに格納できる。

マイグレーションファイル

データベースに変更を指示するためのファイル。

コーディング

ビューファイルで使うコード

<% %>

ex)<%= @post %>

HTMLファイルにRubyコードを埋め込む際に、コードの前後に表記する。コードをブラウザに表示させたい場合は、コードの前後に<%= %>と表記する。

link_toメソッド

<%= link_to("表示する文字", "URL") %>
ex) <%= link_to("投稿一覧", "/posts/index")%>

リンクを作成する。HTMLファイルで使う。
第3引数に{method: "post"}を追加することで、postとして定義されているルーティングにマッチするようになる。

form_tag

<%= form_tag("URL") do %>
 データ
<% end %>

ex)
<%= form_tag("/posts/update") do %>
 <textarea name="content"><%= @post.content %><textarea>
  <input type="submit" value="投稿">
<% end %>

フォームに入力されたデータを、指定したURLに送信することができる。
しかしform_tag単体では意味がなく、textareaタグ(またはinputタグ)にname属性を指定することにより、name属性をキーとしたハッシュをアクション側に伝えることができる。
余談だが、上記のようにtextareaタグの間に値を置いておくことで、フォームを再入力する際にもともと入力していた内容の続きから入力できたりする。

errors.full_messages

<% インスタンス名.errors.full_messages.each do |message| %>
  <%= message %>
<% end %>

ex)
<% @posts.errors.full_messages.each do |message| %>
  <%= message %>
<% end %>

エラーメッセージを出力する。saveメソッドを呼び出した際にバリデーションに失敗すると、Railsでは自動的にエラーメッセージが生成されるようになっているため、each文を用いることですべて表示することができる。

yield

views/layout/application.html.erb
<%= yield %>

各ビューファイルは、application.html.erb内に表記されたyieldに代入される。
application.html.erbはサイト全体に適用するレイアウトを記載するビューファイル。

コントローラファイルで使うコード

@変数

@post = "hogehoge"

コントローラファイルで定義をすると、ビューファイルでRubyコードの埋め込みをする際に利用できるようになる。

newメソッド

モデル名.new(カラム名: )
ex) post = Post.new(content: "hogehoge")

モデルからインスタンスを作成する。

saveメソッド

インスタンス名.save
ex) post.save

作成したインスタンスをテーブルに保存する。

allメソッド

モデル名.all
ex) posts = Post.all

テーブル内の全てのレコードを取得する。

find_byメソッド

モデル名.find_by(カラム名: )
ex) post = Post.find_by(id: 1)

ある条件に合致するデータを1つ取得する。

whereメソッド

モデル名.where(カラム名: )
ex) posts = Post.where(id: 1)

ある条件に合致する複数のデータを取得する。

redirect_toメソッド

redirect_to("URL")
ex) redirect_to("/posts/index")

指定したページに転送することができる。

renderメソッド

render("フォルダ名/ファイル名")
ex) render("posts/edit")

別のアクションを経由せずに、直接ビューを表示することができる。
データの保存に失敗したときなどによく使われる。

orderメソッド

モデル名.order(カラム名: :並び替えの順序)
ex) @posts = Post.all.order(created_at: :desc) 

取得したデータの並び替えを行う。:desc は降順、:asc は昇順を表す。

destroyメソッド

インスタンス名.destroy
ex) post.destroy

指定したデータ(インスタンス)をデータベースから削除する。

変数session

session[:キー名] = 
ex) session[:user_id] = @user.id

ページを移動してもログインユーザーの情報を保持し続けるためのもの。
nilを代入するとログイン状態ではないようにすることができる。

params

ex1) @id = params[:id]
ex2) @post = Post.new(content: params[:content])

用例1. ルーティングで設定したURLの :○○ の値を取得する。
用例2. name="○○"がついたフォームの入力内容を受け取る。

before_action

before_action 全アクションで共通する処理
ex) before_action :set_current_user, {only: [:edit, :update]}

どのアクションを呼び出す前でも、必ず記述した処理が実行される。ファイルの一番上に記述する。
{only: [:アクション名]} を用いることで、処理が実行されるアクションを限定することができる。

ルーティングファイルで使うコード

getメソッド

アプリ名/config/routes.rb
get "URL" => "コントローラ名#アクション名"
ex) get "/posts/index" => "posts#index"

指定した情報を取得する。データベースを変更しない場合は大体getを使う。getと下記のpostは、HTTPメソッドと呼ばれている。

postメソッド

アプリ名/config/routes.rb
post "URL" => "コントローラ名#アクション名"
ex) post "/posts/create" => "posts#create"

データベースを変更する際、sessionの値を変更する際に使うメソッド。
HTTPメソッドは他にもいろいろあるらしいが、まだ学んでいないのでここでは省略。

名前付きパラメータ

アプリ名/config/routes.rb
get "posts/:id" => "posts#show"

ルーティングのURL部分に : で始まる文字列を置くと、その文字列はパラメータとして認識される。
そのため、この例でいえば、/posts/○○ のようなすべてのURLをshowアクションに誘導することができる。
その関係で /posts/index のようなルーティングは、/posts/:id のルーティングに引っかからないようその前に記述する必要がある。

モデルファイルで使うコード

validates

validates :検証するカラム名, {検証する内容}
ex) validates :content, {presence: true}

不正なデータがデータベースに保存されないように、データをチェックする(バリデーション)。
主な検証内容は以下の通り。

検証内容 意味
presecse: true そのカラムの値が存在するかどうかをチェックする
length: {maximum: 文字数} 規定した文字数以上のデータは保存できないようにする
uniqueness: true 重複したデータがデータベースに存在するかどうかチェックする

マイグレーションファイルで使うコード

add_column :テーブル名, :カラム名, :データ型
ex) add_column :users, :image_name, :string

既存のテーブルにカラムを追加する。changeメソッドの中に書く。

コマンドライン

  • rails new アプリ名
    入力したアプリの名前と同名のフォルダが作成され、その中に開発に必要なフォルダやファイルが用意される。

  • rails server
    サーバーの起動。

  • rails g controller コントローラ名 アクション名
    コントローラの作成。gはgenerateでも可。
    コントローラ名とアクション名は、HTTPメソッド(URL)と同じ名前を付けることが多い。

  • rails g model モデル名 カラム名:データ型
    マイグレーションファイルとモデルファイルの作成。
    モデル名は単数形かつ一文字目を大文字にする。

  • rails g migration ファイル名
    マイグレーションファイルのみを作成。
    ファイル名は add_image_name_to_users などの分かりやすい名前にする。

  • rails db:migrate
    データベースへのマイグレーションファイルの反映。
    マイグレーションファイルを作ったのにこれを実行しないとエラーが発生する。

まとめ

progateはよいです。

参考にした記事

参考にした書籍

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

progateで習得したRailsの基礎知識まとめ

どんな人向けの記事か

  • Railsを学び始めて間もない人
  • Railsで初めてアプリを作ろうとしている人
  • Rails慣れはしているけど少し復習したい人

どんな記事か

Rails初学者である筆者が、先日修了したprogateのRailsコースで学んだことを並べていく記事です。
ところどころ他記事の情報で補完しているため、progate運営が表現・説明した内容とは異なる箇所もそれなりにあります。

各種概念

サーバー

クライアント(ブラウザ)の要求に応じてデータを提供するコンピュータ。
Railsで作ったアプリをブラウザで表示するためには、このサーバーを起動させる必要がある。

ビュー

ページの見た目を作るためのHTMLファイル。ブラウザとRailsのやりとりの中で、Railsからビューが返されることで、ページが表示される。
拡張子は.html.erb。erbはEmbedded Rubyの略で、HTMLファイルにRubyスクリプトを埋め込むための仕組み。
下記のコントローラ・ルーティングと同様に、ページを作成するのに必要な3つの要素のうちの1つ。

コントローラ

ブラウザにビューを返すためのもの。コントローラは目的に合わせてそれぞれ作成される。
例えば投稿に関する機能とユーザーに関する機能はそれぞれ別のコントローラファイルで規定される。

アクション

コントローラ内の、ブラウザに返すビューを見つけ出すための機能。コントローラファイル内でメソッドとして表現される。

ルーティング

送信されたURLに対して、「どのコントローラ」の「どのアクション」で処理するかを決める、対応表のようなもの。

データベース

データを保存しておくための場所。

テーブル

データベース内でデータを管理している表。行ごとのデータをレコード、列ごとのデータをカラムと呼ぶ。

モデル

データベースとのやりとりを行うクラス。モデルのインスタンスを作り、それを保存する工程を経ることでデータをテーブルに格納できる。

マイグレーションファイル

データベースに変更を指示するためのファイル。

コーディング

ビューファイルで使うコード

<% %>

ex)<%= @post %>

HTMLファイルにRubyコードを埋め込む際に、コードの前後に表記する。コードをブラウザに表示させたい場合は、コードの前後に<%= %>と表記する。

link_toメソッド

<%= link_to("表示する文字", "URL") %>
ex) <%= link_to("投稿一覧", "/posts/index")%>

リンクを作成する。HTMLファイルで使う。
第3引数に{method: "post"}を追加することで、postとして定義されているルーティングにマッチするようになる。

form_tag

<%= form_tag("URL") do %>
 データ
<% end %>

ex)
<%= form_tag("/posts/update") do %>
 <textarea name="content"><%= @post.content %><textarea>
  <input type="submit" value="投稿">
<% end %>

フォームに入力されたデータを、指定したURLに送信することができる。
しかしform_tag単体では意味がなく、textareaタグ(またはinputタグ)にname属性を指定することにより、name属性をキーとしたハッシュをアクション側に伝えることができる。
余談だが、上記のようにtextareaタグの間に値を置いておくことで、フォームを再入力する際にもともと入力していた内容の続きから入力できたりする。

errors.full_messages

<% インスタンス名.errors.full_messages.each do |message| %>
  <%= message %>
<% end %>

ex)
<% @posts.errors.full_messages.each do |message| %>
  <%= message %>
<% end %>

エラーメッセージを出力する。saveメソッドを呼び出した際にバリデーションに失敗すると、Railsでは自動的にエラーメッセージが生成されるようになっているため、each文を用いることですべて表示することができる。

yield

views/layout/application.html.erb
<%= yield %>

各ビューファイルは、application.html.erb内に表記されたyieldに代入される。
application.html.erbはサイト全体に適用するレイアウトを記載するビューファイル。

コントローラファイルで使うコード

@変数

@post = "hogehoge"

コントローラファイルで定義をすると、ビューファイルでRubyコードの埋め込みをする際に利用できるようになる。

newメソッド

モデル名.new(カラム名: )
ex) post = Post.new(content: "hogehoge")

モデルからインスタンスを作成する。

saveメソッド

インスタンス名.save
ex) post.save

作成したインスタンスをテーブルに保存する。

allメソッド

モデル名.all
ex) posts = Post.all

テーブル内の全てのレコードを取得する。

find_byメソッド

モデル名.find_by(カラム名: )
ex) post = Post.find_by(id: 1)

ある条件に合致するデータを1つ取得する。

whereメソッド

モデル名.where(カラム名: )
ex) posts = Post.where(id: 1)

ある条件に合致する複数のデータを取得する。

redirect_toメソッド

redirect_to("URL")
ex) redirect_to("/posts/index")

指定したページに転送することができる。

renderメソッド

render("フォルダ名/ファイル名")
ex) render("posts/edit")

別のアクションを経由せずに、直接ビューを表示することができる。
データの保存に失敗したときなどによく使われる。

orderメソッド

モデル名.order(カラム名: :並び替えの順序)
ex) @posts = Post.all.order(created_at: :desc) 

取得したデータの並び替えを行う。:desc は降順、:asc は昇順を表す。

destroyメソッド

インスタンス名.destroy
ex) post.destroy

指定したデータ(インスタンス)をデータベースから削除する。

変数session

session[:キー名] = 
ex) session[:user_id] = @user.id

ページを移動してもログインユーザーの情報を保持し続けるためのもの。
nilを代入するとログイン状態ではないようにすることができる。

params

ex1) @id = params[:id]
ex2) @post = Post.new(content: params[:content])

用例1. ルーティングで設定したURLの :○○ の値を取得する。
用例2. name="○○"がついたフォームの入力内容を受け取る。

before_action

before_action 全アクションで共通する処理
ex) before_action :set_current_user, {only: [:edit, :update]}

どのアクションを呼び出す前でも、必ず記述した処理が実行される。ファイルの一番上に記述する。
{only: [:アクション名]} を用いることで、処理が実行されるアクションを限定することができる。

ルーティングファイルで使うコード

getメソッド

アプリ名/config/routes.rb
get "URL" => "コントローラ名#アクション名"
ex) get "/posts/index" => "posts#index"

指定した情報を取得する。データベースを変更しない場合は大体getを使う。getと下記のpostは、HTTPメソッドと呼ばれている。

postメソッド

アプリ名/config/routes.rb
post "URL" => "コントローラ名#アクション名"
ex) post "/posts/create" => "posts#create"

データベースを変更する際、sessionの値を変更する際に使うメソッド。
HTTPメソッドは他にもいろいろあるらしいが、まだ学んでいないのでここでは省略。

名前付きパラメータ

アプリ名/config/routes.rb
get "posts/:id" => "posts#show"

ルーティングのURL部分に : で始まる文字列を置くと、その文字列はパラメータとして認識される。
そのため、この例でいえば、/posts/○○ のようなすべてのURLをshowアクションに誘導することができる。
その関係で /posts/index のようなルーティングは、/posts/:id のルーティングに引っかからないようその前に記述する必要がある。

モデルファイルで使うコード

validates

validates :検証するカラム名, {検証する内容}
ex) validates :content, {presence: true}

不正なデータがデータベースに保存されないように、データをチェックする(バリデーション)。
主な検証内容は以下の通り。

検証内容 意味
presecse: true そのカラムの値が存在するかどうかをチェックする
length: {maximum: 文字数} 規定した文字数以上のデータは保存できないようにする
uniqueness: true 重複したデータがデータベースに存在するかどうかチェックする

マイグレーションファイルで使うコード

add_column :テーブル名, :カラム名, :データ型
ex) add_column :users, :image_name, :string

既存のテーブルにカラムを追加する。changeメソッドの中に書く。

コマンドライン

  • rails new アプリ名
    入力したアプリの名前と同名のフォルダが作成され、その中に開発に必要なフォルダやファイルが用意される。

  • rails server
    サーバーの起動。

  • rails g controller コントローラ名 アクション名
    コントローラの作成。gはgenerateでも可。
    コントローラ名とアクション名は、HTTPメソッド(URL)と同じ名前を付けることが多い。

  • rails g model モデル名 カラム名:データ型
    マイグレーションファイルとモデルファイルの作成。
    モデル名は単数形かつ一文字目を大文字にする。

  • rails g migration ファイル名
    マイグレーションファイルのみを作成。
    ファイル名は add_image_name_to_users などの分かりやすい名前にする。

  • rails db:migrate
    データベースへのマイグレーションファイルの反映。
    マイグレーションファイルを作ったのにこれを実行しないとエラーが発生する。

まとめ

progateはよいです。

参考にした記事

参考にした書籍

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

Rubyでアルゴリズムを実装する:Day 4 -線形探索-

3日坊主を脱却して4日も続きました。
3日目はこちら<Rubyでアルゴリズムを実装する:Day 3 -二分探索->

線形探索とは

ランダムなデータを最初から一つずつ比べて特定の値を見つけていく方法。
見つかったらそこで処理を終了する。
とーっても簡単なアルゴリズムだ
仕組みがわかったら即コーディングに移ろう

linerSearch.rb

コード

# 線形探索

def linerSearch(data, target)
  bool = -1
  count = 0
  while count <= data.length
    if data[count] == target
      bool = count
      break
    end
    count += 1
  end
  bool
end

# 実行
print "格納する値:"
data = gets.split().map(&:to_i)
print "探す値:"
target = gets.to_i
search = linerSearch(data, target)

if search >= 0
  puts "#{target}#{search+1}番目に見つかりました。"
else
  puts "#{target}は見つかりませんでした。"
end

linerSearchは数字が格納された配列と探す値を引数に取る。
配列の位置を示すcountが配列の大きさを超えるまでループ
データが見つかればboolにその時の配列の場所を代入してループ終了。
見つからなければboolは-1を返す。

出力は見つかれば見つかった場所。見つからなければ見つからなかった旨を出力する。

最後に

やっぱり昨日にに比べたらとても簡単だった
ただ、イメージできているものをイメージ通りに実装するのって難しいなって思ったりもする

さて、次回は再帰でもっとも有名なハノイの塔をやってみるか。。。

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