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

【Ruby】モジュールのミックスインについて

はじめに

今回はRubyのモジュールについてまとめました!
モジュールには様々な用途があります。
主に以下のような用途です。

・クラスにインスタンスメソッドを追加する(ミックスイン)
・クラスにクラスメソッドを追加する(ミックスイン)
・クラス名などの重複を防ぐためにネームスペース(名前空間)を作成する
・ミックスインせずにモジュール単体としてメソッドを定義
など。

最初は何を言ってるか全くわけがわからないと思います。
全ての機能をここで説明するととても長文になってしまうので、今回は上記のうち上から2つ(ミックスイン)に関して説明します。

モジュールとは

ここで説明する”ミックスインの機能を持つモジュール”とは、そもそも何でしょうか。

説明する前に、まずクラスに関してざっとおさらいしておきます。
継承関係にあるクラスは、親クラスのメソッドを子クラスでも呼び出すことが出来ました。
以下がその例です。

test.rb
class User
 def name(name)
  conversation = "hello,#{name}"
 end
end

class Person < User 
end

person = Person.new
person.name("たろう")
#出力結果 ==>"hello,たろう"

親クラスがUserで、子クラスがPersonです。
class Person < UserでUserクラスを継承しています。
その為Personクラスのインスタンスに対して、Userクラスのnameメソッドを呼んだ際に、
ちゃんと"hello,たろう"と返っていることから親クラスのメソッドが使えていることがわかります。

このように、継承していれば、親クラスのメソッドは使用できるのですが、逆に
継承していなければ他クラスのメソッドは使用できないとも言えます。

継承関係にはしたくないけれども、他クラスと全く同じメソッドを使いたい!と言う場合。
継承できないから同じ内容を毎回毎回クラスごとに書かなくてはいけないんでしょうか?
確かにそれでも良いかもしれませんが、コードの量が増えますし、
理想的なコーディングとして重複した処理は避けるべきです。

こんな時に役立つのがまさに!モジュールのミックスイン機能です!!
簡単に説明すると、共通のメソッドをモジュール内に定義しておき、
それをそれぞれのクラスで呼び出して使用することができる機能です。
このモジュールを呼び出すことをミックスインと呼びます。

モジュールのミックスイン

ではさっそくモジュールのミックスインに関して解説します。
とりあえずモジュールの書き方に関して先に見ておいてください。以下のようになります!

test.rb
module モジュール名
 #モジュールの内容(メソッドやクラスなど)
end

書き方はいたって簡単ですね。ではさっそく使い方を解説します!

include

まずはインスタンスメソッドをモジュール内に定義して継承関係ではない2つのクラスにそれぞれ適用するケースです。
module名Helloを最初に定義し、(#---①)
moduleを読み込むためにそれぞれのクラスでincludeします。
include モジュール名と書くだけで読み込むことができます。 (#---②)
Userクラス内のnameメソッド内でHelloモジュールのhelloメソッドを呼び出しています。(#---③)
同じくDogクラス内のnameメソッド内でもHelloモジュールのhelloメソッドを呼び出しています。(#---④)
その後、それぞれのクラスのインスタンスを作成し、nameメソッドを呼び出すと、ちゃんとnameメソッド内のhelloメソッドが呼び出されていることが確認できます。(#---⑤)

test.rb
module Hello   #---①
 def hello(name)
  puts "hello,#{name}"
 end
end

class User
 include Hello   #---②
 def name
  hello("たろう")  #---③
 end 
end

class Dog
 include Hello   #---②
 def name
  hello("ぽち")  #---④
 end 
end

user = User.new
user.name
# 出力結果 ==>"hello,たろう"   #--⑤

dog = Dog.new
dog.name
# 出力結果 ==>"hello,ぽち"   #---⑤

※モジュールもクラスも今回同じファイル内に書いてしまっていますが、もちろん別々にファイルを分けて頂いてOKです!
このように、モジュールで共通のメソッドをひとまとめにすることで、異なるクラスでもモジュール内のインスタンスメソッドが使用できます。

extend

includeはインスタンスメソッドをモジュールから呼び出すために使用しましたが、クラスメソッドの呼び出しにはextendを使用します。
モジュールの定義方法はincludeの時と変わりません。

test.rb
module Hello
 def hello(name)
  puts "hello,#{name}"
 end
end

class User
 extend Hello 
 def self.name
  hello("たろう")
 end 
end

User.name
# 出力結果 ==>"hello,たろう"

つまり、モジュール内のメソッドをインスタンスメソッドとして使用するのか、クラスメソッドとして使用するのかでincludeとexcludeどちらを使うかが決まると言うことですね。
モジュール内の記述は変わらないので、同じモジュール内のメソッドでもクラスによってはインスタンスメソッドあるいはクラスメソッドになりうると言うことですね!とても便利です!

さいごに

今回はモジュールの機能のうち、ミックスインに関して説明させて頂きました!
モジュールには他にも機能があるので、ぜひ気になる方は調べてみてください:relaxed:

【参考】
書籍 プロを目指すためのRuby入門(伊藤 淳一 著)

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

Rails の migration の仕組みを理解する

普段 Rails で開発をしていて、migration の仕組みって、日付で migration ファイルが作成されて管理されてるってイメージで確認したことなかったので、動かしながら確認してみた。

とりあえず rails new してアプリを立ち上げるところまではやって DB を作るところからいじってみた。
DBは PostgreSQL を使っています。

初期状態

rails
➜  rails_migration git:(master) bin/rails db:create
Created database 'rails_migration_development'

この時点ではまだテーブルはない。

postgresql
rails_migration_development=# \d
Did not find any relations.

migration を実行して中身を確認

とりあえず model を作成

rails
➜  rails_migration git:(master) bin/rails g model Article title body
      invoke  active_record
      create    db/migrate/20190707095850_create_articles.rb
      create    app/models/article.rb

➜  rails_migration git:(master) ✗ bin/rails db:migrate
== 20190707095850 CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0129s
== 20190707095850 CreateArticles: migrated (0.0131s) ==========================

すると 4 つテーブルができる。今回見ていくのは schema_migrations

postgresql
rails_migration_development=# \d
                 List of relations
 Schema |         Name         |   Type   | Owner
--------+----------------------+----------+--------
 public | ar_internal_metadata | table    | ikepon
 public | articles             | table    | ikepon
 public | articles_id_seq      | sequence | ikepon
 public | schema_migrations    | table    | ikepon

この schema_migrations には version ってカラムが一つだけある。

postgresql
rails_migration_development=# \d schema_migrations
               Table "public.schema_migrations"
 Column  |       Type        | Collation | Nullable | Default
---------+-------------------+-----------+----------+---------
 version | character varying |           | not null |
Indexes:
    "schema_migrations_pkey" PRIMARY KEY, btree (version)

中の値を見ると、さっきの migration ファイルの日付の情報が入ってる。
(db/migrate/20190707095850_create_articles.rb)

postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
    version
----------------
 20190707095850
(1 row)

rollback してみる

これを rollback すると値も消える。

rails
➜  rails_migration git:(master) ✗ bin/rails db:rollback
== 20190707095850 CreateArticles: reverting ===================================
-- drop_table(:articles)
   -> 0.0245s
== 20190707095850 CreateArticles: reverted (0.0287s) ==========================
postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
 version
---------
(0 rows)

もう一つテーブルを追加

テーブルを2つにして確認してみる

rails
➜  rails_migration git:(master) ✗ bin/rails g model User name email
      invoke  active_record
      create    db/migrate/20190707101406_create_users.rb
      create    app/models/user.rb
➜  rails_migration git:(master) ✗ bin/rails db:migrate
== 20190707095850 CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0129s
== 20190707095850 CreateArticles: migrated (0.0131s) ==========================

== 20190707101406 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0054s
== 20190707101406 CreateUsers: migrated (0.0056s) =============================

すると PostgreSQL には 2つの version ができる。
(db/migrate/20190707095850_create_articles.rb)
(db/migrate/20190707101406_create_users.rb)

postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
    version
----------------
 20190707095850
 20190707101406
(2 rows)

日付が前の migration を実行してみる

rollback して user のファイル名を
db/migrate/20190707101406_create_users.rb から
db/migrate/20190706101406_create_users.rb に修正して migration を実行する。

すると変更した日付の値が入る。

postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
    version
----------------
 20190707095850
 20190706101406
(2 rows)

version にない日付の migration は実行されないと判断されて、実行される。

これを bin/rails db:rollback すると新しいものから消えていく

postgresql
rails_migration_development=# SELECT * FROM schema_migrations;
    version
----------------
 20190706101406
(1 row)

同じファイルで日付を変えてみる

bin/rails db:migrate でテーブルを戻しておいて、 user のファイルをコピーして
db/migrate/20190707101406_create_users.rb と一番新しい日付に修正後 migration を実行する。

rails
➜  rails_migration git:(master) ✗ bin/rails db:migrate
rails aborted!
ActiveRecord::DuplicateMigrationNameError:

Multiple migrations have the name CreateUsers.

migration の名前を変えてみる

Multiple migrations have the name CreateUsers. 同じ名前の migration があるって言われてるので、 migration の名前を class CreateUser < ActiveRecord::Migration[5.2] (Users の s を削除)に変えて実行してみる。

するとエラーが変わった。

rails
➜  rails_migration git:(master) ✗ bin/rails db:migrate
== 20190707101406 CreateUser: migrating =======================================
-- create_table(:users)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::DuplicateTable: ERROR:  relation "users" already exists

さらに古い方(db/migrate/20190706101406_create_users.rb) を削除して再実行してみる。

rails
➜  rails_migration git:(master) ✗ bin/rails db:migrate
== 20190707101406 CreateUser: migrating ======================================
-- create_table(:users)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::DuplicateTable: ERROR:  relation "users" already exists

まとめ

  • migration ファイルは日付で判断して、 schema_migrations にないものを実行するようになっている。
  • migration 内では同じ名前の class は使えない。
class CreateUser < ActiveRecord::Migration[5.2]
    # ~~~~~~~~~~ この部分

改めて手を動かして、テーブルのデータを確認しながら見ていくとちゃんと理解できた。

参考

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

rake db:migrateができない件について

rake db:migrateができない・・・!

経緯

deviseを使用し、ログイン機能を実装中
マイグレーションファイルに、追加のカラムを記入後
rake db:migrateをターミナルで行うとエラーが発生する。

これまでの流れ

  1. deviseをGemfileに追記し、 bundle installを実行する
  2. rails g devise:installを実行する
  3. rails g devise userを実行し、テーブルを作成する
  4. マイグレーションファイルに追加したいカラムを記入する image.png
  5. rake db:migrateを実行する
  6. 下記エラーが発生する
$ rake db:migrate
rake aborted!
ActiveRecord::NoDatabaseError: Unknown database 'アプリ名_development'


Caused by:
Mysql2::Error: Unknown database 'アプリ名_development'

Tasks: TOP => db:migrate
(See full trace by running task with --trace)

データベースがないと怒られてしまった。

7. 下記を参考に、rails db:createを実行し、データベースを作成する
  https://rails-ambassador.herokuapp.com/debugs/NoDatabaseError
  https://blog.f-arts.work/archives/600

$ rails db:create
Created database 'アプリ名_development'
Created database 'アプリ名_test'

8.今度こそ!と、rake db:migrateを実行するとエラー発生

== 20190707130850 DeviseCreateUsers: migrating ================================
-- create_table(:users)
rake aborted!
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Cannot add foreign key constraint: CREATE TABLE `users` (`id` int AUTO_INCREMENT PRIMARY KEY, `nickname` varchar(255) NOT NULL, `email` varchar(255) DEFAULT '' NOT NULL, `encrypted_password` varchar(255) DEFAULT '' NOT NULL, `user_image_id_id` int NOT NULL, `user_detail_id_id` int NOT NULL, `reset_password_token` varchar(255), `reset_password_sent_at` datetime, `remember_created_at` datetime, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL,  INDEX `index_users_on_user_image_id_id`  (`user_image_id_id`),  INDEX `index_users_on_user_detail_id_id`  (`user_detail_id_id`), CONSTRAINT `fk_rails_938ec9574d`
FOREIGN KEY (`user_image_id_id`)
  REFERENCES `user_image_ids` (`id`)
, CONSTRAINT `fk_rails_ce7e2c90f2`
FOREIGN KEY (`user_detail_id_id`)
  REFERENCES `user_detail_ids` (`id`)
) ENGINE=InnoDB
/Users/自分/projects/アプリ名/db/migrate/20190707130850_devise_create_users.rb:5:in `change'

Caused by:
ActiveRecord::StatementInvalid: Mysql2::Error: Cannot add foreign key constraint: CREATE TABLE `users` (`id` int AUTO_INCREMENT PRIMARY KEY, `nickname` varchar(255) NOT NULL, `email` varchar(255) DEFAULT '' NOT NULL, `encrypted_password` varchar(255) DEFAULT '' NOT NULL, `user_image_id_id` int NOT NULL, `user_detail_id_id` int NOT NULL, `reset_password_token` varchar(255), `reset_password_sent_at` datetime, `remember_created_at` datetime, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL,  INDEX `index_users_on_user_image_id_id`  (`user_image_id_id`),  INDEX `index_users_on_user_detail_id_id`  (`user_detail_id_id`), CONSTRAINT `fk_rails_938ec9574d`
FOREIGN KEY (`user_image_id_id`)
  REFERENCES `user_image_ids` (`id`)
, CONSTRAINT `fk_rails_ce7e2c90f2`
FOREIGN KEY (`user_detail_id_id`)
  REFERENCES `user_detail_ids` (`id`)
) ENGINE=InnoDB
/Users/自分/projects/アプリ名/db/migrate/20190707130850_devise_create_users.rb:5:in `change'

Caused by:
Mysql2::Error: Cannot add foreign key constraint
/Users/自分/projects/アプリ名/db/migrate/20190707130850_devise_create_users.rb:5:in `change'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)

外部キー設定しているけど、参照先ないよ!って怒られました・・・そりゃあそうだ・・・
外部キー設定しているカラムは、一旦削除してrake db:migrateを実行したら、無事作成できました。

== 20190707130850 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0207s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0219s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0118s
== 20190707130850 DeviseCreateUsers: migrated (0.0547s) =======================

http://localhost:3000/users/sign_in
を開くと

image.png

とりあえず、最低限のログインページはOK!

参考

外部キー制約の書き方
https://qiita.com/ryouzi/items/2682e7e8a86fd2b1ae47

Rails「ユーザーのモデルを作成する」
https://qiita.com/macotok/items/a17a4b0d22db4e885678

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

HTMLでテーブルのヘッダー(上部と左部)を固定してスクロールさせる実装

はじめに

テーブルのヘッダー(上部と左部)を固定してスクロールさせる実装でプラグインなどを比較したため、備忘録を含めまとめたいと思います。

position:stickの実装方法

まず、大前提として本来であればposition:stickyを使用することが望ましいかと思いました。
こちらの方の記事が大変わかりやすいです。
https://qiita.com/s0tter/items/14fb4ec2600828a21a22
CSSのみで完結するため上記手法が望ましいかと考えておりますが、2019年7月7日時点でIEに対応しておりません。IEのユーザーもアプリを活用されることが予想されるため、採用を見送りさせていただきました。
https://caniuse.com/#search=position%3A%20sticky

position:sticky対応状況

スクリーンショット 2019-07-07 17.43.02.png

可能であれば、position:stickyを使用したいのですが、今回はIEの関係で採用が難しいため、以下のプラグイン等を比較いたしました。

プラグインを比較してみました。

当方Macしかパソコンを持ち合わせていないため、virtualBoxでIEを立ち上げて
動作状況を確認してみました。

・DataTable

Github:https://github.com/DataTables/DataTables
https://datatables.net/extensions/fixedcolumns/examples/initialisation/two_columns.html
最終更新:2018年6月23日

IEでの動作状況

ezgif.com-video-to-gif.gif

・fixedTblHdrLftCol

Github:https://github.com/nkmrshn/fixedTblHdrLftCol
http://nkmrshn.com/fixedTblHdrLftCol/samples/sample_3_sync.html
最終更新:2014年6月13日

IEでの動作状況

ezgif.com-video-to-gif.gif

・FixwdMidashi

http://hp.vector.co.jp/authors/VA056612/fixed_midashi/manual/index.html
最終更新:2018年12月3日

IEでの動作状況

ezgif.com-video-to-gif.gif

・Grid

(まだまとめている最中です。)

IEで最も綺麗に動作しているのが、FixwdMidashiだったように思えます。
他に良い方法などございましたら、一声かけていただけますと幸いです。

少しずつ追加させていただきます。
ご参考になれば幸いです。

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

Don't know how to build task 'YOUR CREATE TASK'

ファイル拡張子を rake にしましょう!

rbにしてた:upside_down:

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

Herokuの本番環境でRansack検索した時にエラー「PG::SyntaxError: ERROR: syntax error at or near "DISTINCT"」

問題:Herokuの本番環境でRansack検索した時にエラー「PG::SyntaxError: ERROR: syntax error at or near "DISTINCT"」

ローカルでは問題ないのに本番環境で検索すると
「We're sorry, but something went wrong.」のエラーが出てしまった

ログを見てみるとこんな感じ

ActionView::Template::Error (PG::SyntaxError: ERROR:  syntax error at or near "DISTINCT"
2019-07-07T08:13:50.904602+00:00 app[web.1]: LINE 1: SELECT DISTINCT DISTINCT ON (latitude)* FROM "events" WHERE ...
2019-07-07T08:13:50.904604+00:00 app[web.1]: ^
2019-07-07T08:13:50.904606+00:00 app[web.1]: : SELECT DISTINCT DISTINCT ON (latitude)* FROM "events" WHERE "events"."prefecture_id" = $1 AND "events"."play_type" IN (1) ORDER BY latitude,id asc):

distinct付近に何かしら問題があるぽい
controllerはこんな感じ

class PrefecturesController < ApplicationController
  def show
    @prefecture = Prefecture.find(params[:id])
    @events = @prefecture.events
    @q = Event.ransack
    if Rails.env == 'production'
      @venues = @events.select("DISTINCT ON (latitude)*").order("latitude,id asc")
    else
      @venues = @events.group(:latitude).order("id asc")
    end
    gon.venues = @venues
    duplicate = @events.group(:latitude).having('count(*)>=2').pluck(:latitude)
    gon.events = Event.where(latitude: duplicate).offset(1)
  end

  def search
    @prefecture = Prefecture.find(params[:id])
    @q = @prefecture.events.ransack(search_params)
    @events = @q.result(distinct: true)
    if Rails.env == 'production'
      @venues = @events.select("DISTINCT ON (latitude)*").order("latitude,id asc")
    else
      @venues = @events.group(:latitude).order("id asc")
    end
    gon.venues = @venues
    duplicate = @events.group(:latitude).having('count(*)>=2').pluck(:latitude)
    gon.events = @events.where(latitude: duplicate).offset(1)
  end

  private

  def search_params
    params.require(:q).permit(:title_cont,{:dayw_in => []},{:level_in => []},{:play_type_in => []},:status_eq)
  end
end

showとsearchアクションはご覧の通り殆ど一緒です
違うのは検索クエリ?の@q変数が関わっているところだけ
その中でdistinct使ってるのはこの一文のみ

@events = @q.result(distinct: true)

もしかしたらこいつのせいかもと思って消してみたら...
動いたー

(distinct: true)の意味を調べてみる

そもそも(distinct: true)をオプションでつけると何が変わるのか確認
重複したレコードを除外してくれらしい
SQLはこんな感じ

SELECT DISTINCT `events`.* FROM `events` WHERE `events`.`prefecture_id` = 22 AND `events`.`play_type` IN (1)
SELECT `events`.* FROM `events` WHERE `events`.`prefecture_id` = 22 AND `events`.`play_type` IN (1)

仮説

@events = @q.result(distinct: true)

この一文で全てのカラムが重複しないレコードを取り出しているのに

@venues = @events.select("DISTINCT ON (latitude)*").order("latitude,id asc")

ここでまたlatitudeが一意のデータを抽出しようとしてるからエラーになったんだと思う

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

プログラミング初学者が独学でポートフォリオを完成させるまでの話~ポートフォリオ制作の流れ~

自己紹介

2019年3月初旬にエンジニアYouTuberの動画を見てHTML/CSSからProgateでプログラミングの勉強を始め、2019年6月末にRuby on Railsのポートフォリオを完成させてデプロイしました。2019年4月からは大学院の修士課程(農学)にいて、21卒でエンジニアを目指しています。プログラミングの勉強を始めた2019年3月時点ではプログラミングは完全未経験でした。
本記事では独学での勉強の流れや、はまりやすいことを書いているのでプログラミング学習の計画を立てる際の参考にしていただければと思います。また、ポートフォリオ制作の際にできなかったことも正直に書いているので、本記事を読むことで「こいつもこれができなかったけれど、こいつが最後まで独学できたんだから自分だってできる」というような感じで初学者の励みになればと思います。

独学の大まかな流れ(筆者の場合)

  1. 2019年3月にエンジニアYouTuberの動画を見てエンジニアが稼げることを知り、Progateを始める
  2. プログラミングスクールに行こうとしていたが、3月下旬、あるスクールの担当者の提案から独学でポートフォリオ制作を頑張る方向に切り替える
  3. 4月中旬にポートフォリオの制作を開始し、ゴールデンウィーク期間中は連日ポートフォリオ制作を進める
  4. ゴールデンウィークが明けてからは忙しく、なかなかポートフォリオ制作ができない時期が続く
  5. 6月に入って忙しさがひと段落し、ポートフォリオ制作を再開。6月末にポートフォリオが完成し、デプロイ

各時期にしたことや、そのとき感じたこと

1. 2019年3月にエンジニアYouTuberの動画を見てエンジニアが稼げることを知り、Progateを始める

見ての通り、筆者はかなりミーハーな理由でプログラミングを始めています(笑)。今はエンジニア不足のため、きちんと戦力になるのであれば、そのような理由でプログラミングの勉強を始めてもよいのではないでしょうか。
プログラミングの勉強を始める前に、プログラミング言語の選択です。いろんな人の記事や動画を見て、どの言語にするかを総合的に判断しましょう。言語の人気などの情勢はすぐに変わるものなので、自分でしっかり調べて最新の情勢や行きたい企業をもとに判断するとよいと思います。筆者はRubyを選びました。
Progateの勉強法については、筆者はまず学習コースを一通りやった後、コードを残した状態で復習していました。そして、復習する際には書いたコードを説明とともに自分専用のライブラリとしてPCに保存していました。コードの意味を一度に理解しようとはせず、復習でコードを保存する際にある程度理解することを目標にしていました。エンジニアになりたいと思ってProgateをするのであれば、Progateには課金しましょう。きちんと集中して学習すれば課金は1000円で済みます。筆者は忙しさを言い訳にダラダラ学習していたので3000円課金しましたが(笑)
また、筆者は2019年3月までは大学4年生であり、2019年4月からは修士に進学することが決まっていました。しかし、エンジニアYouTuberの動画を見始めるようになってからは、大学院を中退してプログラミングスクールで勉強し、そのまま働こうと思うようになっていました。今となってはこの選択は捨て身の非常に危険な選択であることが分かりましたが、世間知らずの筆者は当時、この選択の危うさが分かっていませんでした。そんな筆者の間違った方向性を修正してくれた、ある出会いがあったのです。

2. プログラミングスクールに行こうとしていたが、3月下旬、あるスクールの担当者の提案から独学でポートフォリオ制作を頑張る方向に切り替える

筆者は大学院を中退してそのまま働こうと思っていくつかのプログラミングスクールに話を聞きに行っていました。しかし、DMM WEB CAMPさんに説明を受けに行ったところ、個人面談の担当者の方から「新卒採用まで待った方がいい。修士の間にポートフォリオを作りなさい。新卒採用でポートフォリオを作ってくる人はほとんどいないから、そこでポートフォリオがあると優位になる。今後のことを考えると、新卒カードは絶対に捨てない方がいい。」という内容のことを言われました。このアドバイスは自分たちのプログラミングスクールに入らない方が良いと言っている内容であり、ビジネスチャンスを自分から断っていることになります。短期的な自分たちの利益を度外視しても筆者のためになるアドバイスをしていただき、本当に感謝しています。筆者はこのアドバイスのおかげで極端な選択をやめて現実的な方向に行けました。プログラミングスクールを選ぶ際には、一度DMM WEB CAMPさんにお話を伺いに行くと親身にアドバイスしていただけると思いますので、おすすめです。

3. 4月中旬にポートフォリオの制作を開始し、ゴールデンウィーク期間中は連日ポートフォリオ制作を進める

かくして筆者は独学でポートフォリオを作る方針に切り替えました。Progateでの勉強が一通り終わったので、ポートフォリオの制作を開始しました。とは言っても、ライブラリ作りがまだ終わっていなかったのでProgateへの課金は継続していました。
Progateは学習編と道場編がありますが、Railsコースの道場編は面倒臭くなったのでしませんでした。筆者の場合はライブラリさえ作れれば道場編をしなくてもポートフォリオを作れたので、道場編をしなくてもポートフォリオは作れます。しかし、実際にポートフォリオを作る段階になるとProgateの道場編以上に多くのエラーが出てくるので、ポートフォリオ制作で極力詰まりたくない人は道場編もしっかりしておくのが無難かもしれません。
ポートフォリオ制作の前にRubyとRailsの環境構築をしてRailsの初期画面を自分のPC上に表示させますが、これがかなり大変でした。Aという作業をするためにはBとCをインストールしないといけないが、BをインストールするためにはDとEとFが必要、といった感じで芋づる式に必要なものがたくさん出てくるため、自分が何をしているのか途中で分からなくなります。環境構築で挫折する人が一定数いるというのも納得で、環境構築が自分できちんとできた人は独学でポートフォリオを完成させることができるだけのリサーチ力と忍耐力があると思います。(あとはやる気が続くかどうか次第です)
ポートフォリオ制作の最初はProgateのコードのコピペです。筆者はProgateでRailsの道場コースをしていなかったため、ポートフォリオを作りながらコードの意味を理解していきました。そして意外だったのが、Progateのコードを完全にコピペしたコードでもエラーが出ることです。ググったところ、Progateのコードが必ずしもベストなコードではないことが分かったり、Rubyのバグが原因(Rubyのバージョンを切り替えると解決しました)だったりしました。
ゴールデンウィークの期間中に一気にポートフォリオ制作を進め、Progateのコピペ作業はゴールデンウィーク期間中に終わりました。

4. ゴールデンウィークが明けてからは忙しく、なかなかポートフォリオ制作ができない時期が続く

エンジニアYouTuberやプログラミングスクールの人たちは会社に入るまでの学習期間を3か月程度と言っている人が多かったので、3月初旬の勉強開始から約3か月後になる5月末にポートフォリオを完成させようとしていました。しかし、連休が明けてからは大学院の方で課題の提出ラッシュがあったために忙しく、ポートフォリオの制作時間がなかなか取れない時期が続いていました。ポートフォリオ制作のモチベが下がることもありましたが、そういう時はエンジニアYouTuberの動画を見返してモチベを上げていました。エンジニアYouTuberたちは極端なことを言っていることもあるので(お前が言うなというツッコミはさておき)内容を安易に鵜呑みにしない方がいいと思いますが、プログラミングのモチベを維持するためには結構いいものです。

5. 6月に入って忙しさがひと段落し、ポートフォリオ制作を再開。6月末にポートフォリオが完成し、デプロイ

6月に入ると大学院での課題の提出ラッシュが終わって時間に余裕ができ、週末を中心にポートフォリオ作りをしていました。この頃になるとProgateで勉強していない機能を実装しようと頑張っていました。筆者の場合は、勝又健太さんがポートフォリオに入れた方がよい機能一覧として挙げている機能を一通り実装し、ポートフォリオの内容から筆者が追加したいと思った機能も追加しようとしていました。勝又さんは欲しい機能を10個ほど挙げていますが、Progateの内容ですでに実装できている機能が割と多いので、実装方法を最初からググらなければいけない機能は意外と少ないです。そのため、一部難易度が高い機能もありますがこれからポートフォリオを作る皆さんも勝又さんの挙げている機能一覧に挑戦してみてはどうでしょうか。
Progateでは扱っていない機能を実装する際には、当然のことながら実装方法をググります。すると、大抵の場合はすぐに実装方法を丁寧に書いてくれているページが見つかるのですが、大変なのはここからです。実装手順が5つぐらい書いてある場合、99%の確率で途中どこかでエラーが出ます。そのエラーの対処に時間がかかることも多く、ここは踏ん張りどころですね。
筆者の場合はほとんどの機能は実装できたのですが、テスト機能においてテストコード内でログインする方法が分からず、最終的にテスト機能の実装をあきらめました。このように実装をあきらめる機能が出てくるのは本来あまり良くないことですが、筆者は最後まで独学でポートフォリオを作ることを優先したので、挫折しないためにこのようにしました。独学だとこのような妥協が必要になることもあるかもしれません。また、実装をあきらめる機能があってもProgateにはない機能がポートフォリオにある方がいいため、ポートフォリオの設計段階ではポートフォリオに入れたい機能を多めに挙げておくといいかもしれません。
最終的にポートフォリオを完成させることができたらGitHubで公開します。実はこの記事を書いている2019年7月時点ではProgateのGitコースは無料で受講できるので、Progateをすでに解約していたとしてもGitHubの使い方をProgateで勉強できます。Gitの基礎はProgateで勉強し、分からないことがあったらググるというのが一番簡単だと思います。
筆者は最終的にはHerokuというサービスでポートフォリオをデプロイ(インターネット上で公開すること)しました。Railsアプリの場合、デプロイの前にマイグレーションファイルをカラムの過不足がないように一度整理しておくことをおすすめします。また、コードは合っているのにHerokuページ上だけエラーが出る場合はデータカラムがないことが原因かもしれないということを覚えておくといいかもしれません。
ポートフォリオを完成できた方は、おめでとうございます。ポートフォリオを持って企業の面接を受けに行きましょう。幸運を祈ります!

最後に

筆者の場合は学業で忙しかった時期もあったことから勉強開始からポートフォリオ完成まで約4か月という少し長い期間がかかり、途中モチベーションが下がることもありました。仕事が忙しいなどといった理由で学習が長期化するのは仕方ないことだと思いますが、そうなった場合はモチベーション低下との戦いになります。モチベーションが下がったときは自分がプログラミングの勉強を始めたきっかけになったものを見返すといいかもしれません。ただ、ポートフォリオ完成までできた人は比較的短期間で勉強している人が多いので、短期集中の方が挫折しにくいことを覚えておくといいかもしれません。
長い記事になってしまいましたが、最後まで読んでくださり、ありがとうございました。本記事が参考になりましたら、いいねを押していただけると幸いです。
それではこれからプログラミングを始めるみなさん、まずは独学でポートフォリオを作ることに挑戦してみましょう。幸運を祈ります!

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

rubyのブロック処理における配列関連のメソッドメモ

チェリー本を参考にした。このあたりよく忘れるのでメモっておく

each

普通のfor文と同じ役割

num = [1,2,3,4]
sum = 0
num.each do |n|
  sum += n
end
print sum #=> 10

each_with_index

eachについて配列のインデックスも扱うことができる

num = [1,2,3,4]
num.each_with_index do |i,n|
  p "#{i}:#{n}"
end
#出力は以下の通り
#0:1
#1:2
#2:3
#3:4

map

ある配列の要素に対する処理結果を別配列に返す

num = [1,2,3,4]
new_num = []
new_num = num.map do |n|
  n * 10
end
print new_num #=> [10,20,30,40]

エイリアスメソッドであるcollectも同様の使い方が可能

select/find_all

ある配列の要素について、条件に一致しているものを返す

num = [1,2,3,4]
new_num = num.select do |n|
  n.odd? #nが奇数のものをnew_numへ格納
end
print new_num  #=> [1,3]

エイリアスメソッドであるfind_allも同様の使い方が可能

reject

ある配列の要素について、条件に一致していないものを返す
要するにselect/find_allの反対

num = [1,2,3,4]
new_num = num.select do |n|
  n.odd? #nが奇数でないものをnew_numへ格納
end
print new_num  #=> [2,4]

find/detect

ある配列について、戻り値が真かつ最初の要素を返す

num = [1,2,3,4]
new_num = num.find do |n|
  n.odd?
end
print new_num #=> 3

エイリアスメソッドであるdetectも同様の使い方が可能

inject/reduce

ある値を初期値として配列各要素を足しこむ。eachの拡張か。

num = [1,2,3,4]
sum = num.inject(0) do |result,n|
  result += n #初期値0に各要素をすべて足しこむ。初期値が異なれば結果も異なる。
end
print sum #=> 10

当然ながら文字列の結合処理にも利用可能
エイリアスメソッドであるreduceも同様の使い方が可能

その他

map/collectとinject/reduceの思想の違いらしい。エイリアスというくらいなので多分map処理を名前変えてcollectとかしてんだろうな。要するに実装上の違いはないと思うがどうなのか。。。
https://magazine.rubyist.net/articles/0038/0038-MapAndCollect.html#map-%E3%81%A8-collect-%E3%81%AE%E7%99%BA%E6%83%B3%E3%81%AE%E9%81%95%E3%81%84

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

Rubyのゲッター、セッター、initializeについて

car.rb
class Car

  def initialize(color)
    @color = color
  end

  # ゲッター
  def color
    @color
  end

  # セッター
  def color=(color)
    @color = color
  end
end

# インスタンス化する際にinitializeメソッドに値を渡す
car1 = Car.new("red")
car2 = Car.new("green")
car3 = Car.new("blue")
# car1.color = "gray"

# colorを呼び出すと代入された値が表示される
puts car1.color
puts car2.color
puts car3.color

↑こういうrbファイルがあったとする。

これをターミナル上で実行ruby car.rbすると下記のような結果になる

red
green
blue

疑問に思ったこと

initialize(color) は引数(color)
car1 = Car.new("red")の("red")を受け取ることができている。

「えっ・・・これセッターを書く意味ある?」と疑問に思ったのだ。
試しにセッターをコメントアウトして、再度ターミナルの結果を見てみる

car.rb
class Car

  def initialize(color)
    @color = color
  end

  # ゲッター
  def color
    @color
  end

  # セッター
  # def color=(color)
  #   @color = color
  # end
end

# インスタンス化する際にinitializeメソッドに値を渡す
car1 = Car.new("red")
car2 = Car.new("green")
car3 = Car.new("blue")
# car1.color = "gray"

# colorを呼び出すと代入された値が表示される
puts car1.color
puts car2.color
puts car3.color

結果↓

red
green
blue

「えっ・・・これホンマにセッター書く意味ないやん、、、
試しにcar1.color = "gray" でcar1.colorの値もちゃんと返ってくるか確かめてみよ」

car.rb
class Car

  def initialize(color)
    @color = color
  end

  # ゲッター
  def color
    @color
  end

  # セッター
  # def color=(color)
  #   @color = color
  # end
end

# インスタンス化する際にinitializeメソッドに値を渡す
car1 = Car.new("red")
car2 = Car.new("green")
car3 = Car.new("blue")
car1.color = "gray" #ここのコメントアウト外した

# colorを呼び出すと代入された値が表示される
puts car1.color
puts car2.color
puts car3.color

結果↓

Traceback (most recent call last):
class.rb:22:in `<main>': undefined method `color=' for #<Car:0x00007ffe6b172520 @color="red"> (NoMethodError)
Did you mean?  color

「おっ!エラーはいた。(良かった〜)」

わかったこと

initializeメソッドは初期値を受けとれるだけ。
なので今回のcar1.color = "gray"のように後から代入する可能性も考えると
セッターの記述は必要。

ちなみにセッターのコメントアウトも外して、再度実行するとcar1.colorの出力値を更新することができた。

car.rb
class Car

  def initialize(color)
    @color = color
  end

  # ゲッター
  def color
    @color
  end

  # セッターのコメントアウトを外した
  def color=(color)
    @color = color
  end
end

# インスタンス化する際にinitializeメソッドに値を渡す
car1 = Car.new("red")
car2 = Car.new("green")
car3 = Car.new("blue")
car1.color = "gray"

# colorを呼び出すと代入された値が表示される
puts car1.color
puts car2.color
puts car3.color

結果↓

gray
green
blue

オブジェクト指向って難しいね〜(大満足)

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

プログラミング初学者が独学でポートフォリオを完成させるまでの話〜挫折しない方法〜

自己紹介

2019年3月初旬にエンジニアYouTuberの動画を見てHTML/CSSからProgateでプログラミングの勉強を始め、2019年6月末にRuby on Railsのポートフォリオを完成させてデプロイしました。2019年4月からは大学院の修士課程(農学)にいて、21卒でエンジニアを目指しています。プログラミングの勉強を始めた2019年3月時点ではプログラミングは完全未経験でした。
本記事がこれから独学でプログラミングの勉強を始める人の参考になれば嬉しいです。

挫折しないためのポイント

結論から言うと、独学でプログラミングを勉強する際には以下の3点を守るようにすると挫折しにくくなります。
1. お手本のコードを自分専用のライブラリとして保存する
2. 勉強時間は余裕をもって設定する
3. ググっても分からないバグを無理に解決しようとしない

それでは、1つずつ解説していきます。

1. お手本のコードを自分専用のライブラリとして保存する

これは挫折しない方法というよりは勉強法になります。Progateで書いたコードのような正しいコードを解説付きでコンピューターに保存しておきましょう。ポートフォリオを制作する際は、そのコードを一旦コピペしてから必要部分を書き換えるようにすると文法ミス(syntax error)が減ってコードを書く速度が格段に上がります。

2. 勉強時間は余裕をもって設定する

時間に余裕がないことによる焦りは挫折の大きな原因になります。初学者の場合、エンジニアとしての最低限の技術とやる気の証明になるようなポートフォリオを制作するまでの作業時間はProgate等の基礎学習の時間を含めると最低500時間は確保しておいた方が無難です。当然のことながら個人差があるので、もっと時間がかかる人も多くいると思いますので、500時間以上かかったとしてもとにかく焦らないように気をつけましょう。

3. ググっても分からないバグを無理に解決しようとしない

ググっても分からないバグは分からないのですから、解決しようとするだけ無駄です。無理に解決しようとしても消耗するだけです。とはいってもバグは解決しなければいけません。
一通りググってもバグを解決できない場合は以下のような順番で対処することをおすすめします。

3-1. 一晩寝かせて翌朝もう1回そのバグを見てみる

昨晩どれだけググっても解決できなかったバグが、翌朝もう1度見てみると驚くほどあっさり解決することがよくあります。そういう時は基礎的なことが抜けていた場合が本当に多いんですよね…。

この方法で解決できなかった場合、3-2に進んでください。3-2はA,B,Cと分岐します。バグの状況や自分の精神状態(笑)に応じて適切なものを選んでください。

3-2A. プログラミングスクールやプログラミングができる友人などに質問する

解決しなければ全く進めないバグに遭遇したときや挫折しそうな精神状態のときの対処法として推奨する方法です。一番確実に素早く答えを得られる方法ですが、この方法を使うことで失われるものがあることを知っておいた方がよいでしょう。

1. お金

プログラミングスクールは(無料期間のあるスクールがあるものの)基本的に10万円単位のお金がかかります。痛い出費ですが、挫折するよりもはるかにマシなので、選択肢の1つとして頭の片隅に入れておきましょう。友人に聞く場合も、ごはんを奢ってあげた方がいいですよね。

2. 自走能力の証明

初学者が独学で作ったポートフォリオにはエンジニアとしての「自走能力」(バグを自力でググって解決して次に進む能力)の証明という大きな要素があるのですが、プログラミングスクールや友人に質問するとポートフォリオからこの要素がなくなります。実はこちらがかなり大きいそうです。しかし、繰り返しますが挫折するよりもはるかにマシなので、精神的にやばいときはプログラミングスクールに行きましょう。誤解がないように言っておくと、筆者はこの方法は使っておりません。

3-2B. 質問サイトで質問する

teratailStack Overflowなどのプログラミング専用の質問サイトがあるので、そこで質問してみましょう。ただし、質問してもなかなか答えが返ってこないことも多く、答えが得られる保証はありません。ただ、困っている内容を人にわかりやすく伝えるいい機会なので、解決できないバグが出てきたときにはこの方法を一度使ってみることをおすすめします。

3-2C. あきらめる

筆者が多く使った方法(?)です(笑)。ポートフォリオを制作するうえで必須ではない機能の部分で一通りググっても分からないことがあった場合、制作時間の短縮のためにその機能実装をあきらめました。初学者がポートフォリオを取りあえず完成させて形にするうえでは必要なことだと割り切ってこれをするのもありだと思います。筆者は独学にこだわって自走能力の証明をしたかったので、この方法を多く使いました。当然のことながら、「現場で実力がついたら、必ずこの機能を実装する」と誓ってポートフォリオでは実装できなかった機能をコメントアウトして残しています。実装をあきらめた機能があっても、とりあえず独学でポートフォリオを完成させたという事実は大きな自信につながるので、可能なら最後まで独学でポートフォリオを作りましょう。

最後に

最近では技術の発展によってプログラミング独学のハードルがどんどん下がっています。最後まで独学でポートフォリオを完成させられるかどうかに関しては個人の適性もありますが、できれば独学でがんばりましょう。
この記事に書ききれなかったポートフォリオ制作の流れを時系列で書いたものを別記事として作ります。プログラミングを独学する際の大まかな流れや心構えの参考になればと思って書きますので、そちらも読んでいただけると幸いです。
それでは、これからプログラミングの勉強を始めるみなさん、幸運を祈ります!

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

書籍『たのしいRuby』の練習問題を解いてみた Part.2

本題

今回は第13章の練習問題を解いていく。それでは早速解いてみる。
(1) 1~100までの整数が昇順に並ぶ配列aを作れ。

1.rb
a=[]
for i in 1..100
    a<<i
end

aという空の配列に1~100まで詰め込んでいくようにした。

(2) (1)で作成した要素全てを100倍にした配列a2を作成せよ。また、新しい配列を作成せずに、全ての要素を100倍にした要素に置き換えよ。

2_1.rb
a=[]

for i in 1..100
    a<<i
end

a2=a.map{|i| i * 100}

p a2
#実行結果:[100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000, 2100, 2200, 2300, 2400, 2500, 2600, 2700, 2800, 2900, 3000, 3100, 3200, 3300, 3400, 3500, 3600, 3700, 3800, 3900, 4000, 4100, 4200, 4300, 4400, 4500, 4600, 4700, 4800, 4900, 5000, 5100, 5200, 5300, 5400, 5500, 5600, 5700, 5800, 5900, 6000, 6100, 6200, 6300, 6400, 6500, 6600, 6700, 6800, 6900, 7000, 7100, 7200, 7300, 7400, 7500, 7600, 7700, 7800, 7900, 8000, 8100, 8200, 8300, 8400, 8500, 8600, 8700, 8800, 8900, 9000, 9100, 9200, 9300, 9400, 9500, 9600, 9700, 9800, 9900, 10000]

mapメソッドで別の要素に置き換えた。  

2_2.rb
a=[]
for i in 1..100
    a<<i
end

a.map!{|i| i * 100}
p a
#実行結果:[100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000, 2100, 2200, 2300, 2400, 2500, 2600, 2700, 2800, 2900, 3000, 3100, 3200, 3300, 3400, 3500, 3600, 3700, 3800, 3900, 4000, 4100, 4200, 4300, 4400, 4500, 4600, 4700, 4800, 4900, 5000, 5100, 5200, 5300, 5400, 5500, 5600, 5700, 5800, 5900, 6000, 6100, 6200, 6300, 6400, 6500, 6600, 6700, 6800, 6900, 7000, 7100, 7200, 7300, 7400, 7500, 7600, 7700, 7800, 7900, 8000, 8100, 8200, 8300, 8400, 8500, 8600, 8700, 8800, 8900, 9000, 9100, 9200, 9300, 9400, 9500, 9600, 9700, 9800, 9900, 10000]

mapの後に!を付けてレシーバの配列を変更するメソッドを使いました。mapの部分はcollectでも良いと思います。

(3) (1)の配列から3の倍数だけを取り出した新しい配列a3を作れ。また、新しい配列を作らずに3の倍数以外の数を削除せよ。

3_1.rb
a=[]
for i in 1..100
    a<<i
end

a3 = a.select{|i| i % 3 == 0}
#確認のため出力してみる
p a3
#実行結果:[3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99]

Enumerableモジュールの中のselectメソッドを使ってみた。find_allでも同じ挙動する。

3_2.rb
a=[]
for i in 1..100
    a<<i
end

a.delete_if{|i| i % 3 != 0}
p a

aの配列から3の倍数以外を削除しろという事なので、3で割り切れないものを削除するようにした。

(4) (1)の配列を逆順に並べ換えろ。
(5) (1)の配列に含まれる整数の和を求めよ。
(4と5はそこまで難しくないので一緒にします)

4_1.rb
a=[]
for i in 1..100
    a<<i
end
#(5)のコード
p a.sum
#実行結果:5050

#(4)のコード
a.reverse!
p a
#実行結果:[100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

reverseメッソド,sumメソッドを使うだけでした。

(6) 1~100の整数を含む配列aryから、1~10,11~20,21~30というように10個の要素を含む配列を10個取り出します。それを全て配列resultに格納せよ。

6_1.rb
ary = [*1..100]
result = Array.new

10.times do |i|
    result<<ary.slice!(0,10)
end
#出力して確認してみる
p result
#実行結果:[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [11, 12, 13, 14, 15, 16, 17, 18, 19, 20], [21, 22, 23, 24, 25, 26, 27, 28, 29, 30], [31, 32, 33, 34, 35, 36, 37, 38, 39, 40], [41, 42, 43, 44, 45, 46, 47, 48, 49, 50], [51, 52, 53, 54, 55, 56, 57, 58, 59, 60], [61, 62, 63, 64, 65, 66, 67, 68, 69, 70], [71, 72, 73, 74, 75, 76, 77, 78, 79, 80], [81, 82, 83, 84, 85, 86, 87, 88, 89, 90], [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]]

slice!を使い指定した範囲を取り出し、それを一つの要素として配列resultに入れた。

(7) 数値からなる配列nums1とnums2に対して、それらの個々の要素を足し合わせた要素からなる配列を返すメソッドsum_arrayを定義せよ。

7_1.rb
def sum_array(nums1,nums2)
    result=Array.new
    nums1.zip(nums2)do |a , b , c|
        result << a + b
    end
    return result
end
#確認のため実行してみる
p sum_array([8,1,2] , [5,7,4])
#実行結果:[13, 8, 6]

zipメソッドで処理させた。

最後に

今回は配列の操作がメインの内容だった。Enumerableモジュールには色々なメソッドがあるようなので、上手く扱っていきたい。
本自体は読み終わっているので、このまま継続して記事を出していこうと思います。

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

技術系資格試験は受ける意味があるのか?〜Ruby Silver編〜

前書き

世の中には様々な資格試験があります。
そして技術分野にも資格試験があります。
受ける理由は人それぞれだと思いますが、今回は資格試験は意味がある=業務に役立つのかをポイントに、個人的に感じたことをまとめてみます。

前提と先に結果

私の経歴はこんな感じです。

  • Java歴 1.5年 (新卒時です)
  • Ruby/Rails歴 1年ちょっと (厳密には学生時代にも数ヶ月やったことがありました)

ただし仕事ではサービス開発/運用/セキュリティ全般を担当しているので、毎日フルでRubyを書いてるという訳でなく、AWSを触っていたりというよな時間も多いです。(ハードル下げ)

・受けた試験
Ruby技術者認定試験のSilverです。
Goldを取るつもりなのですが、それには下位であるSilverがまず必要だったので受けました。

・受けたきっかけ
自分がどれだけRubyを理解できているか知りたかった、
Rubyの組み込みクラスを再実装する勉強会に行った際に理解が足りていないと感じた、
CTOが良い試験だと言っていたからです。

・勉強時間と結果
勉強期間は2週間弱です。平日0.5~1時間、休日3時間程度です。
業務後に予定があったり、週末旅行に行ったりしたので毎日はできませんでしたが、前日の詰め込みなどを平均したらそれくらいです。
100点中75点が合格点で、90点でした。

Ruby技術者認定試験は受ける意味があったか?

先に結論から言いますと、個人的にはあったと思います。

まずそう思った要因は、
1. この試験は業務に役立つ試験になるように作られている気がした
2. 実務を1年やってから受けているので、単なる暗記にならなかった
3. 達成感を得られる
です。

以下で詳しく説明します。

1.業務に役立つ試験になるように作られている気がした

まず、「こういう記憶色の強い試験だと、重箱を隅を突くような出題があって、無駄な勉強が生まれるのでは」と私は危惧していました。
が、結果的には勉強に無駄が生まれないように工夫されていると感じました。
網羅的にメソッドが紹介されている参考書を読んでいる時、「これそんなに使うシーンあるかな・・」というメソッドがあったりするのですが、肌感ですがそれらは出題されなかったので、テスト作成者も使用頻度が高いメソッドを選んでいるんじゃないかな〜と個人的には感じました。

※そもそもOSSであるRubyにメソッドが用意されているということは、一定以上の需要があるはずです。
なので、「これそんなに使うシーンあるかな・・」と感じるのは私の経験が浅いという理由もあると思っています。

ただ全くなかったわけでなく、1つありました。それはいろんなパターンのHashの生成。
でもこれくらいでした。

2. 実務を1年やってから受けているので、単なる暗記にならなかった

私はもともとJavaをやっており、間髪入れずにRubyの会社に転職をしたので業務をしながら覚えていきました。
入門はたのしいRubyとチェリー本だったので、網羅的に学ぶということはしていませんでした。

合格教本で完全ではありませんが割と網羅的に学べたので、知らなかったメソッドを知れました。その際、「あそこの処理、このメソッドで置き換えた方がcoolかも」と考えながら勉強ができ、実際書き換えもしたのでとても有益でした。

また他によかった点として、毎回出くわす度に調べていた名前や挙動の紛らわしいメソッドをちゃんと覚えられたので、仕事も早くなるかなと思います。
あとはオブジェクト指向についての出題も多いので勉強し直したのですが、改めてJavaと比べてRubyの挙動は柔軟で面白いな〜とRubyをさらに好きになりました笑

3. 達成感を得られる

Silverは踏み台として受けただけなのですが、それでも達成感はありました。

余談ですが、受講料について。
1万6千円は高すぎでしょ!?と思っていたのですが、試験2日ほど前には「1万6千円を無駄にできない」という猛烈なpressureで勉強が捗ったので、大人を本気にさせるとてもいい価格設定だったと思いました。

勉強方法とかかった時間

先人達が色々とまとめられているので簡単に。

私がやったのは、
0. 勉強方法や気をつけるべきポイントを調べる
1. まず1回模擬試験を解いて、自分のできなさ具合に焦る&雰囲気を掴む
2. Ruby公式資格教科書 の該当箇所を読む。私はGoldも受ける気だったので、Silverの範囲はあまり気にせず読んでいました。
3. https://www.ruby.or.jp/ja/certification/examination/rex で模擬試験を4回くらいやる
です。

あとRuby試験に限ったことではないのですが、中学/大学受験時の経験として、
紛らわしいポイントは抽出してまとめておき、1日数回、突如自分クイズを始めてなんども思い出して定着させることがかなり有効なのでやっていました。

まとめ

Ruby技術者認定試験は、業務に活かせるか?というポイントにおいて、個人的には良い試験だと思います。
特に業務を始めて半年くらいたった人には良いと思います。
これからRubyをはじめるきっかけとしてこの試験に取り掛かるのは、個人的には暗記ゲーになってしまい、楽しくできるかは微妙なんじゃないかなと思っています。先にアプリを作ったりしてからやった方がいいかなと思います。もちろん自分がそうだった訳ではないのであくまで一意見ですが。

あとがき

プロメトリックの回し者ではありません。
試験勉強は大好きです。
駆け出しRubyistのヒントになればいいな。

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

Rails で ActiveInteraction を使うメリット

はじめに

ActiveInteraction とは、rails で controller に書かれるビジネスロジックを整理するときに使えるgemです。プロダクトが大きくなるにつれて複雑になり、太った controller や model をスリムにすることができます。また、ActiveModel が名詞でわかりやすく扱えるのと同様に、ActiveInteraction は動詞で命名してわかりやすく扱うことができます。

使いかたについては、ActiveInteraction の README または、こちらの Qiitaの記事 を参考にしてみてください。

この記事では、ActiveInteraction のメリットについて書いていきます。

ActiveInteraction を使うメリット

複雑になりがちな controller を簡潔にできる

ActiveInteraction を使う一番のメリットは、複雑になった controller を簡潔にできることです。

例 : order と report モデルがあり、二つは関連を持っているとします。report は、create した時にステータスによって通知を送るべきかどうか変えたい、という要件があった場合、そのロジックを controller から分離することができます。

(簡略化して書いています)

def create
 #  ここに書くべき内容を ActiveInteraction::Base を継承している create.rb に移動できる
  outcome = Reports::Create.run(reports_create_params)

  if outcome.valid?
    redirect_to order_url(@order), notice: 'create!'
  else
    flash.now[:alert] = 'error!'
    render :new
  end
end
# create.rb
module Reports
  class Create < ActiveInteraction::Base
    object :order, class: Order
    object :account, class: Account
    string :content, default: nil
    array :images, default: []
    string :to_state, default: nil

    def execute
      report = order.reports.build(
        content: content, images: images,
        account: account, date: Time.current.to_date
      )

      ActiveRecord::Base.transaction do
        report.save!

        unless to_state&.to_sym == :accepted
          compose(Notifications::Send)
        end
      end

      report
    end
  end
end

controller がとてもシンプルで読みやすいですね。

複雑な controller は可読性が悪く、バグを生む原因にもなります。

ActiveInteraction を使うことで、controller で書くことをシンプルに保つことができ、ビジネスロジックを名前空間を使って他のところに移動させることができます。

このメリットはservice層を作ることで補うこともできるのですが、ActiveInteraction を使うことでよりシンプルにわかりやすくなります。ちなみに僕の会社では、もともとservice層にロジックを押し込んでいたこともあり、今はservice層という名前空間に ActiveInteraction::Base を継承させて利用しています。

なぜ ActiveInteraction を使うとよりわかりやすくなるのか知りたい方は、こちらの記事をご覧ください。

バグを生みにくい(見つけやすい)

二つ目のメリットは、上記した通り、バグを生みにくいということです。

ActiveInteraction は静的型付けを行います。なので、型が原因で起こるバグを潰すことができます。型が決まっているので、複数人で開発している時でもその変数に何が入っているのか一目瞭然で可読性が高いです。

# このような感じでかけます
object :purchase, class: Order
object :account, class: Account
string :content, default: nil
array :images, default: []

また、Validation をつけることができます。書き方は model と全く同じです。そもそも ActiveInteraction 内で Validation チェックすることがあまりないので利用頻度は高くないと思いますが、こちらもうまく使うことでバグの原因を潰すことができます。

ロジックのテストが描きやすい

三つ目のメリットは、ロジックだけを隔離することにより、テストが描きやすくなることです。
ActiveInteraction は一つのロジックを一つのファイルに納めるので、テストが一つのロジック毎に別れ、シンプルに保つことができます。また、テストする部分を明確にすることができます。

最後に

ActiveInteraction は README がとてもわかりやすいので、困ったことがあれば大体は README で解決できます。

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

Ruby の Hash#select, reject のブロックパラメーター

Ruby のハッシュの select, reject などのメソッドについて,公式リファレンスにも載っていない「あれ?」と思った仕様に気付いたので。

おさらい

まず,Hash の selectreject がどんなメソッドなのかを振り返る。

Ruby 1.8 時代は仕様が違ったり,Ruby 2.1 の頃に黒歴史があったりしたが,この記事では最近のバージョンだけを考慮することにする。とりあえず Ruby 2.6 だ。(現時点で公式サポートが継続している Ruby 2.4 以降なら同じだと思う)

Hash には Enumerable が include してあるが,selectreject は Hash で独自に実装されており,Enumerable#selectEnumerable#reject が使われることはない。

Hash#select は自身の要素から特定の条件を満たす要素だけを採用した新たなハッシュを生成して返すもの。
Hash#reject はその論理反転であり,自身の要素から特定の条件を満たす要素だけを取り除いた新たなハッシュを生成して返すもの。
いずれも条件はブロックで与える。

以下の例では,条件として,キーが 'ba' を含み,かつ値が正であるもの,としよう。

hash = {foo: 3, bar: 4, baz: 0}

p hash.select{ |key, value| (/ba/ =~ key) && (value > 0) }
# => {:bar=>4}

p hash.reject{ |key, value| (/ba/ =~ key) && (value > 0) }
# => {:foo=>3, :baz=>0}

これで動作は一目瞭然。

公式リファレンス(Ruby 2.6.0)は以下のとおり。

reject のほうの説明は「self を複製し」とあるが,これは間違いではないかと思う。selectreject もデフォルト値は引き継がれないし,Hash のサブクラスに適用してもサブクラスでなく Hash を返すから。

なお,ブロックを与えない使い方もあるが,本題に関係ないので割愛。

ブロックパラメーターは一つでも

ここからが本題。
これらのメソッドは,前節のコード例のように,ブロックパラメーターとしてキーと値の二つを使うのが普通だ。公式リファレンスでもそのような例しか載っていない。

しかし,実はブロックパラメーター一つの使い方がある。

hash = {foo: 3, bar: 4, baz: 0}

p hash.select{ |key| /ba/ =~ key }
# => {:bar=>4, :baz=>0}

p hash.reject{ |key| /ba/ =~ key }
# => {:foo=>3}

見てのとおり,ブロックパラメーターを一つにした場合,要素のキーが渡ってくる。

これに気付いたとき,「え?」と思った。
ブロックパラメーターが二つであるべきところに一つしか記述しなかったら,配列の形で渡ってくるんじゃないのか?(後述するがこの認識は間違い)
だって,Hash#each だったら

hash = {foo: 3, bar: 4}

hash.each do |a, b|
  p a, b
end
# => :foo
# => 3
# => :bar
# => 4

hash.each do |a|
  p a
end
# => [:foo, 3]
# => [:bar, 4]

になるよね?

てことは,Hash#selectHash#reject は与えられたブロックのパラメーターの数1で挙動を変えているのか?(次節で述べるようにこれは誤解)

特別な動作ではなかった

結論を先に言えば,ブロックパラメーターを一つだけ指定したときにキーだけが渡ってくるのは,特別な動作でも何でもなかった。だから公式リファレンスにも特に言及はなかったわけだ。

すっかり忘れていたが,メソッドから引数二つで yield したとき,ブロックパラメーターが一つしかなかったら,yield の最初の引数だけが渡され,二つ目は無視されるのだった。

つまりこういうこと:

def foo
  yield 1, 2
end

foo{ |x| p x } # => 1

これは Ruby の yield の仕様であって,特定のクラスとかには関係ない。

ではなぜ,私は配列が渡ってくるはずと勘違いしたのだろうか?
原因は明らかに Hash#each の動作を見たためだが,Hash#each の動作はどのように理解すればいいのだろうか?

どうやら,Hash#each は,キーと値を引数として yield しているのではなく,キーと値からなる配列を引数として yield しているらしい。
いや,Hash#each は Ruby でなく C で実装しているから,そもそも yield なんか呼んでいないのだが,「Ruby コードとして表現すれば」という話である。

Ruby 2.6 の Hash#each の実装は↓ここであるらしい。
https://github.com/ruby/ruby/blob/ruby_2_6/hash.c#L2779-L2810

私は C が読めないのでよく分からないが,ぼんやりとは分かる。
ブロックパラメーターの数で場合分けしてあるのは高速化のためのようだが,ともかく 0 個や 1 個の場合は each_pair_i が呼ばれる。
each_pair_i の実装は↓ここ。
https://github.com/ruby/ruby/blob/ruby_2_6/hash.c#L2762-L2767

ここに

rb_yield(rb_assoc_new(key, value));

とある。rb_assoc_new というのは引数をつなげた配列(?)を作る関数ぽいので,やはり配列を yield しているのだ。

ブロックパラメーターが 2 個以上のときは each_pair_i_fast が呼ばれるが,その実装は↓ここ。
https://github.com/ruby/ruby/blob/ruby_2_6/hash.c#L2769-L2777

よく分からないが,こちらも配列を作っているぽい。

Ruby では,配列を yield した場合,ブロックパラメーターが 1 個ならその配列がそのまま渡され,2 個なら展開されて渡される,という仕様であった。
つまりこういうこと:

def foo
  yield [1, 2]
end

foo{ |a| p a }
# => [1, 2]

foo{ |a, b| p a; p b }
# => 1
# => 2

これで謎が解けた。

どちらかといえば,与えるブロックパラメーターの数について注意が必要なのは Hash#each のほうであったか。


  1. ブロックが持つブロックパラメーターの数をそのブロックの arityアリティー という。一般にコンピューター用語で,関数などの引数が 1 個,2 個,3 個であることを unary, binary, ternary という。arity はここから作られた単語で,関数などの引数の個数のこと。Ruby ではメソッド側が,与えられたブロックのアリティーを知る手段を持っている。「ていに申せ」ってね。 

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

#Ruby の then / yield_self は メソッドチェーンで nil を防止するための初期値代入にも使えそうだけど?

Ruby's then / yield_self could also be used for initial value assignment to prevent nil in the method chain?

題材は多分不適切

こんなので nil を防ぎたい時

numbers = [1,2,3]; numbers&.max.yield_self { |max| max || 0 } + 1
# => 4

then / yield_self での書き方

numbers = nil; numbers.then { |numbers| numbers || [] }.max.then { |max| max || 0 } + 1
# => 1

numbers = nil; numbers.yield_self { |numbers| numbers || [] }.max.yield_self { |max| max || 0 } + 1
# => 1

かっこでくくる + ぼっち演算子での書き方

numbers = nil; (numbers&.max || 0) + 1
# => 1

Original by Github issue

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

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

リクエストのスタブ化ができる WebMock の使い方を簡単に確認する

はじめに

外部 API のリクエストを含むテストを試したいと思ったときに、Webmock という gem でリクエストをスタブ化できることを知りました。

有名な gem なのかもしれませんが、使ったことがまだなかったので、簡単に使い方を試してみました。

前提環境

  • Mac OS X 10.14.4
  • Rails 5.2.2.1
  • ruby 2.5.5p157 (2019-03-15 revision 67260)]

試し方

今回は WebMock の使い方をすぐに確認したかったので、予め準備しておいたサンプルの Rails アプリのコンソール上で、 Webmock のメソッドの使い方を確認します。

手順

インストール

Gemfile に Webmock をインストールします。

Gemfile
gem 'webmock'

下準備

rails c でコンソールを起動したら、Webmock を利用できるよう、有効化の設定を入れます。

require 'webmock'
include WebMock::API
WebMock.enable!

こちらによって、WebMock のスタブへのリクエストに切り替わります。

README のサンプルを試す

下準備が終わったら、README を参考に、いくつかサンプルを試します。

URL のみのスタブリクエスト

以下のコードで、ダミーの URL である www.example.com へのすべてのリクエストが許可される形になります。

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

クエリパラメータ付き URL のみのスタブリクエスト

クエリパラメータなど、変数に当たる部分は {?} という形で囲むことで使用することができます。

uri_template = Addressable::Template.new "www.example.com/users{?name}"
stub_request(:any, uri_template)

Net::HTTP.get('www.example.com', '/users?name=hoge') 
# => ""

ダミーのレスポンスを指定

リクエスト先の URL に対してダミーのレスポンスを指定できます。

stub_request(:any, "www.example.com")
  .to_return(body: "aaa", status: 200 )
Net::HTTP.get('www.example.com', '/') 
# => "aaa"

レスポンスをテキストファイルで指定

別で保存しておいたテキストファイルをレスポンスとして指定することもできます。

File.open('/tmp/response.txt', 'w') { |f| f.puts 'bbb' }

stub_request(:any, "www.example.com").
  to_return(body: File.new('/tmp/response.txt'), status: 200)

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

おわりに

こちらを応用すれば、 外部通信が伴う API クライアントが絡んだテストも、リクエストの URL さえわかればスタブ化してテストすることができそうです。

今回は試していませんが、リクエストヘッダありの POST リクエストもスタブ化できるようなので、利用の幅が広いなと感じました。

これから試してみようと思います。

参考

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

たのしいOSSコードリーディング: Let's read "cookies"?

この記事は2019年7月6日に開催されたTama Ruby会議での発表「たのしいOSSコードリーディング: Let's read "cookies"?」を詳細解説するものです。

Railsアプリケーションで使用されるcookiesメソッドを題材に、このメソッドがどのように実装されているかを読んでいきます。
読みきれなかった部分・知識が曖昧な部分が残っているため、先輩方からの技術的指摘をお待ちしています。

当日の発表資料はこちら

調査環境

  • Rails 6.0.0.rc1
  • TraceLocation 0.9.3.1

そもそもcookiesメソッドとは

cookies[:hoge] = "fuga"

Railsが用意しているメソッド。
Cookieに「hoge=fuga」を設定するときに使用。

事前知識

①Cookie

クライアントからサーバーへリクエストを送る際、HTTPリクエストヘッダはCookieヘッダを含んでいる。

クライアント?‍? →  リクエスト[Cookie: hoge=fuga]   → サーバー?‍?

Cookieヘッダの中身はこんな感じ。

NAME1=OPAQUE_STRING1; NAME2=OPAQUE_STRING2 ...

サーバーからクライアントにレスポンスを送る際、HTTPレスポンスヘッダはSet-Cookieヘッダを含んでいる。

クライアント?‍? ← レスポンス[Set-Cookie: hoge=fuga] ← サーバー?‍?

Set-Cookieヘッダの中身はこんな感じ。

NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; ...

いずれの場合も、Cookieは次のように名前と値を一つのペアとしている?

Cookieの名前=Cookieの値

(Rubyスクリプトの中で扱う際、ハッシュの形式{名前: 値}に変換したりする)

②Rackミドルウェア

Rackとは

  • 「Rack」は次のふたつの意味を持っている
    • WebアプリケーションとWebサーバーを繋ぐプロトコル
    • WebアプリケーションとWebサーバーを繋ぐライブラリ
  • Railsアプリケーションは、「Rackプロトコルを満たし、かつRackライブラリを内部で使うRackアプリケーション」

Rackミドルウェアとは

  • Webアプリケーションが持っているべき、「特定の汎用的な機能」を切り出したRackライブラリ
  • Railsアプリケーションは多くのRackミドルウェアを使用している
  • 今回見ていくActionDispatch::CookiesはRailsで使用されているRackミドルウェアのひとつ
$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
 ()...

ActionDispatch::Cookiesとは?

  • Railsアプリケーションで使用されているRackミドルウェアのひとつ
  • Cookieを保存するために使う
$ rails middleware
 ()...
use ActionDispatch::Cookies # ←これ
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes

それでは早速読んでいきましょう?

cookies[:hoge] = "fuga"の全体図

trace_locationを使用すると、cookiesメソッドを呼んだ際、次のように一連の処理が走ることが確認できます。

Logged by TraceLocation gem at 2019-06-05 20:31:31 +0900
https://github.com/yhirano55/trace_location

[Tracing events] C: Call, R: Return

C actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12 [ActionController::Cookies#cookies]
  C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11 [ActionDispatch::Request#cookie_jar]
    C rack-2.0.7/lib/rack/request.rb:58 [Rack::Request::Env#fetch_header]
    R rack-2.0.7/lib/rack/request.rb:60 [Rack::Request::Env#fetch_header]
  R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:15 [ActionDispatch::Request#cookie_jar]
R actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:14 [ActionController::Cookies#cookies]
C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374 [ActionDispatch::Cookies::CookieJar#[]=]
  C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:350 [ActionDispatch::Cookies::CookieJar#handle_options]
  R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:370 [ActionDispatch::Cookies::CookieJar#handle_options]
R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:392 [ActionDispatch::Cookies::CookieJar#[]=]

Result: fuga

要約するとこんな感じ

[Rails]ActionDispatch::Cookiesミドルウェアにアクセスする

[Rails][Rack]リクエストから既存のCookieヘッダを見つけて返す
             見つからなかった場合は空のCookieをセットする

[Rails]ActionDispatch::Cookiesのインスタンスを生成
     インスタンス変数を初期化する

[Rails]ActionDispatch::Cookiesのインスタンス変数に
     新しいCookieの値を追加する

[Rails]ミドルウェアがアプリケーションを返すタイミングで
     インスタンス変数の内容をSet-Cookieヘッダに書き込む(?)  ここは処理を追いきれず…

RailsとRackの処理を行ったり来たりするため、手元にソースコードがある方は確認しながら進むことをお勧めします。

ひとつずつ読んでいきましょう?

rails/actionpack/lib/action_controller/metal/cookies.rb

      7 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12#cookies

Rails側から処理を追います?
まずはActionController::Cookies#cookiesメソッドを読んでみましょう。

ActionController::Cookies#cookies

actionpack/lib/action_controller/metal/cookies.rb
 11     private
 12       def cookies
 13         request.cookie_jar
 14       end

一番最初に呼ばれる#cookiesメソッドの中身はこんな感じ、レシーバに対して#cookie_jarメソッドを呼んでいるだけ。

レシーバのrequestActionDispatch::Requestのインスタンス(※include先で定義されている)。

続いて#cookie_jarメソッドを読んでみましょう。

rails/actionpack/lib/action_dispatch/middleware/cookies.rb

      8   C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11#cookie_jar

引き続きRails?

ActionDispatch::Request#cookie_jar

actionpack/lib/action_dispatch/middleware/cookies.rb
 11     def cookie_jar
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
 14       end
 15     end

#cookie_jarメソッドの中身はこんな感じ。
一行目でRackのメソッドであるfetch_header(Rack::Request::Env#fetch_header)を呼んでいます。

続いてRack::Request::Env#fetch_headerを読んでみましょう。

rack/lib/rack/request.rb

      9     C /vendor/bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/request.rb:58#fetch_header

Rack側に移ります?

Rack::Request::Env#fetch_header

rack/lib/rack/request.rb
     69       def fetch_header(name, &block)
     70         @env.fetch(name, &block)
     71       end

#fetch_headerメソッドの中身はこんな感じ。
ここで登場するオブジェクトを確認しましょう。

nameとは

引数のname"action_dispatch.cookies"

(参考)

actionpack/lib/action_dispatch/middleware/cookies.rb
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
 14       end

&blockとは

引数の&blockfetch_headerに渡されているブロック
self.cookie_jar = Cookies::CookieJar.build(self, cookies)

(参考)

actionpack/lib/action_dispatch/middleware/cookies.rb
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies) 
 14       end

レシーバ@envとは

HTTPヘッダを表すハッシュ。
例えばこんなRackアプリがあった場合

['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]

真ん中の{'Content-Type' => 'text/html'}というハッシュが、Rack上では@envというインスタンス変数に入っています。

つまり…

fetch_headerは、ヘッダである@envハッシュに対して、Hash#fetchメソッドを呼ぶための処理。

すなわち

  • @env"action_dispatch.cookies"というkeyを持っていた場合:
    • ➡︎そのvalueが返る
  • @env"action_dispatch.cookies"というkeyを持っていなかった場合;
    • ➡︎ブロックself.cookie_jar = Cookies::CookieJar.build(self, cookies)が実行される

続いて、ブロックself.cookie_jar = Cookies::CookieJar.build(self, cookies)が実行された場合の処理を追います。

rails/actionpack/lib/action_dispatch/middleware/cookies.rb

self.cookie_jar =は、ActionDispatch::Request#cookie_jar=としてメソッド定義されています。
早速読んでみましょう?

ActionDispatch::Request#cookie_jar=

actionpack/lib/action_dispatch/middleware/cookies.rb
     28     def cookie_jar=(jar)
     29       set_header "action_dispatch.cookies", jar
     30     end

ここで登場する引数のjarの正体はCookies::CookieJar.build(self, cookies)ですが、この処理は後で読むので一旦パス。

二行目で、再びRackのメソッドであるset_headerを呼んでいます。

#set_headerの処理を読んでみましょう。

rack/lib/rack/request.rb

再びRackへ?

Rack::Request::Env#set_header

lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end

#fetch_headerメソッドの中身はこんな感じ。
ここで登場するオブジェクトを確認しましょう。

引数nameとは

引数のnameは"action_dispatch.cookies"

引数vとは

vは先程のjar(つまりCookies::CookieJar.build(self, cookies))

具体的には

@env["action_dispatch.cookies"] = Cookies::CookieJar.build(self, cookies)

という処理が走っています。
このとき、返り値はCookies::CookieJar.build(self, cookies)になります。

ここまでのまとめ

アプリケーションでcookiesメソッドが呼ばれると、次の2つの処理が走る。
1. @envハッシュにおける"action_dispatch.cookies"keyの存在を確認する。
2. 存在する場合はvalueを返す。
存在しない場合は新たに{"action_dispatch.cookies":
*Cookies::CookieJar.build(self, cookies)*}
というペアをつくる。

Let's read Rack::Request::Helpers#cookies?

ここからは、先ほどパスしたCookies::CookieJar.build(self, cookies)を読んでいきます。

ActionDispatch::Cookies::CookieJar.build

actionpack/lib/action_dispatch/middleware/cookies.rb
     11     def cookie_jar
     12       fetch_header("action_dispatch.cookies") do
     13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
     14       end
     15     end

↑13行目でself.cookie_jar =に渡しているCookies::CookieJar.build(self, cookies)

が、その前に

注目:eyes:

Cookies::CookieJar.build(self, cookies)

ここで引数に入れたものは何?

(self, cookies)

selfは一番最初に出てきたrequest
cookiesは、これもRackのメソッドを呼んでいます。

rack/lib/rack/request.rb

cookiesの正体を探るため、Rack::Request::Helpers#cookiesメソッドを読んでいきましょう?

Rack::Request::Helpers#cookies

lib/rack/request.rb
    215       def cookies
    216         hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
    217           set_header(k, {})
    218         end
    219         string = get_header HTTP_COOKIE
    220 
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
    222         hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE
    223         set_header(RACK_REQUEST_COOKIE_STRING, string)
    224         hash
    225       end

#cookiesの中身はこんな感じ。

一行づつ

lib/rack/request.rb
    216         hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
    217           set_header(k, {})
    218         end

fatch_headerは先程と同じメソッドで、@envハッシュに対してRACK_REQUEST_COOKIE_HASHkeyをfetchします。

  • keyがあった場合
    • ➡️valueを返す
  • keyがなかった場合
    • ➡️これも先程登場したset_headerメソッドを使用して、 @envハッシュにRACK_REQUEST_COOKIE_HASH: {}というペアを追加する
    • 返り値は空の{}

(参考)

lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end

いずれの場合も、返り値をhashに代入しています。

RACK_REQUEST_COOKIE_HASHとは?

ここで登場する、RACK_REQUEST_COOKIE_HASHの正体は、rack.request.cookie_hash
Rackがサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)

example.rb
{"_todo_session"=>"BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg==--c34c5fc6de928cde391cccd2b710547c7aab1d06"}

(↑の例はたまたま下記の記事中で見つけたものです)
Rails で捕捉されない例外が発生したらメールを送る

つづきまして

lib/rack/request.rb
    219         string = get_header HTTP_COOKIE

HTTP_COOKIEを引数として、get_headerメソッドを呼んでいます。
get_headerRack::Request::Env#get_headerで、定義は以下の通り。

lib/rack/request.rb
    63          def get_header(name)
    64            @env[name]
    65          end

@envハッシュからHTTP_COOKIEkeyを探し、valueを返し、返り値がstringに代入されます。
ただし、初めてCookieを使用する場合は@envハッシュにHTTP_COOKIEkeyが見つからないため、nilが返ります。

HTTP_COOKIEとは?

ここで登場するHTTP_COOKIEの正体は、リクエストメッセージに含まれるメタ変数で、Cookieヘッダを返しています。
中身はこんな感じ(Cookieの名前と値を=で結ぶことでペアとして表現している)
(例)

example.rb _todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06

(↑の例も先ほどの記事よりお借りしました)

※メタ変数について:
参照

例えば HTTP というプロトコルにおいては、 HTTP_USER_AGENT という名前のメタ変数が HTTP 要求メッセージの User-Agent: 欄の値を提供することになっています

つづき

初めてCookieを使用する場合、ここまでで

  • hash{}
  • stringget_header HTTP_COOKIEの返り値(初めてCookieを使用する場合はnil

が代入されていることになります。

lib/rack/request.rb
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)

RACK_REQUEST_COOKIE_STRINGを引数に、Rack::Request::Env#get_headerメソッドを呼んでstringと比較。

(参考)

lib/rack/request.rb
    63          def get_header(name)
    64            @env[name]
    65          end

RACK_REQUEST_COOKIE_STRINGとは?

ここで登場するRACK_REQUEST_COOKIE_STRINGの正体はrack.request.cookie_stringで、こちらもサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)

example.rb
_todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06

(御多分に洩れず↑の例も先ほどの記事より)

HTTP_COOKIEと同じですね!

つづき

lib/rack/request.rb
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)

==の場合はここでreturn hash

なぜ?

次行以降はリクエストヘッダからやって来たhash(初めてのCookieを使用する場合は空の{})をHTTP_COOKIEで更新する処理となっています。
ハッシュの中身が変更されていない場合、置き換える必要がないため。

初めてCookieを使用する場合、stringget_header(RACK_REQUEST_COOKIE_STRING)の返り値はいずれもnilのため、ここでcookiesメソッドの処理はhash(空の{})を返して終了します。

つづき(すでにCookieが存在する場合のみ)

lib/rack/request.rb
    222         hash.replace Utils.parse_cookies_header string

ここでは、
Utils.parse_cookies_header(string)の返り値{ Cookieの名前: Cookieの値 }で、hash(fetch_header(RACK_REQUEST_COOKIE_HASH)の返り値)をreplaceしています。

Hash#replace

Utils.parse_cookies_headerRack::Utilsのメソッドで、
詳しくは読みませんが、
string(Cookieの名前=Cookieの値)を{Cookieの名前: Cookieの値}
のようなハッシュに変換する役割を担っています。

(参考)

lib/rack/utils.rb
    209     def parse_cookies_header(header)
    215       cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
    216       cookies.each_with_object({}) { |(k, v), hash| hash[k] = Array === v ? v.first : v }
    217     end

(↑each_with_objectを使用して{}に名前と値を代入している)

つづき(すでにCookieが存在する場合のみ)

lib/rack/request.rb
    223         set_header(RACK_REQUEST_COOKIE_STRING, string)
    224         hash
    225       end

すなわち、ここで

@env[RACK_REQUEST_COOKIE_STRING] = string

という処理を実行。

(参考)

lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end

最後にhash({Cookieの名前: Cookieの値}) を返しています。

ここまでのまとめ

メソッドcookiesを呼ぶとき

  • 初めてCookieを使用する場合: ヘッダ(@env)に{RACK_REQUEST_COOKIE_HASH: {} }が追加される
    • 最終的な返り値は空の{}
  • すでにCookieが存在する場合: ヘッダ(@env)に{RACK_REQUEST_COOKIE_STRING: リクエストメッセージのCookieヘッダ}が追加される
    • 最終的な返り値は{Cookieの名前: Cookieの値}

改めましてActionDispatch::Cookies::CookieJar.build

actionpack/lib/action_dispatch/middleware/cookies.rb

お待たせしました。Cookies::CookieJar.buildを読んでいきましょう。
再びRailsへ?

ActionDispatch::Cookies::CookieJar.build

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    289       def self.build(req, cookies)
    290         new(req).tap do |jar|
    291           jar.update(cookies)
    292         end
    293       end

Cookies::CookieJar.buildの中身はこんな感じ。

tapを使用して処理をチェーンしているのは、最終的にできあがったCookies::CookieJarインスタンスを返すためではないかと考えられます。
tapを挟まない場合、返り値がjar.updateの実行結果になる)

ここで登場するオブジェクトを確認しましょう。

reqとは

引数のreqは一番最初のrequestActionDispatch::Requestのインスタンス)

cookiesとは

cookiesは先ほど読んだRack::Request::Helpers#cookiesの返り値({Cookieの名前: Cookieの値}あるいは空の{})

Cookies::CookieJar.buildは何をやっているのか

ここでの役割は2つ

  • Cookies::CoookieJarのインスタンスを作る
  • できたインスタンスに対してCookies::CookieJar#updateメソッドを呼ぶ

順番に処理を読んでいきます。

new(req)

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    289       def self.build(req, cookies)
    290         new(req).tap do |jar| # ←
    291           jar.update(cookies)
    292         end
    293       end

new(req)(インスタンス化)すると、initializeが呼ばれます。

actionpack/lib/action_dispatch/middleware/cookies.rb
    295       attr_reader :request
    296 
    297       def initialize(request)
    298         @set_cookies = {}
    299         @delete_cookies = {}
    300         @request = request
    301         @cookies = {}
    302         @committed = false
    303       end

ActionDispatch::Cookies::CookieJar#initializeは各インスタンス変数を初期化するだけの処理。

@requestには引数で渡されている一番最初のrequestが入ります。
attr_readerによってメソッドとしてアクセスできるようになります。

つづいてtap以下の処理

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    290         new(req).tap do |jar|
    291           jar.update(cookies) # ←
    292         end

tapブロックの中で実行されているupdateメソッドはこちら

actionpack/lib/action_dispatch/middleware/cookies.rb
    334       def update(other_hash)
    335         @cookies.update other_hash.stringify_keys
    336         self
    337       end

引数other_hashとは

引数other_hashは先ほどのcookies、つまり{Cookieの名前: Cookieの値}もしくは空の{}

@cookiesとは

@cookiesinitializeの中で初期化したこの部分

    301         @cookies = {}

ここでは、この空の{}に対してupdateメソッドを呼んでいます。
Hash#updateメソッドはHash#merge!のエイリアス。

処理を実行すると、こうなります。

@cookies = {'Cookieの名前': 'Cookieの値'}

(other_hashが空の{}の場合は、何も起きない)

その後、self (=jar、つまりCookies::CookieJar.buildでできたインスタンス)を返しています。

(余談)
↑の方でtapを使った理由を「インスタンスを返すため」と記述しているのですが、ここでもインスタンス自身を返しているため、どちらか片方の処理で良いのではという疑惑あり…PRチャンス?

また、先述の通りこのインスタンスは

actionpack/lib/action_dispatch/middleware/cookies.rb
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)

ここでself.cookie_jar=メソッドに引数として渡されて、

actionpack/lib/action_dispatch/middleware/cookies.rb
     28     def cookie_jar=(jar)
     29       set_header "action_dispatch.cookies", jar
     30     end

ここで@env = {"action_dispatch.cookies": できあがったインスタンス}としてヘッダーに追加されます。

ここまでのまとめ

アプリケーションからcookiesメソッドを呼ぶと、結果的には次のようなリクエストヘッダができる

@env = {
  "action_dispatch.cookies": Cookies::CookieJarのインスタンス},
  # 初めてCookieを使用する場合
  RACK_REQUEST_COOKIE_HASH: {},
  # すでにCookieが存在する場合
  RACK_REQUEST_COOKIE_STRING: リクエストメッセージから受け取ったCookieヘッダ
}

最後に[]=

cookies[:hoge] = :fuga

cookiesに値を代入する処理を見ていきます?

actionpack/lib/action_dispatch/middleware/cookies.rb

14 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374#[]=

ここからはRailsのお話?
#[]=メソッドを読んでいきましょう。

ActionDispatch::Cookies::CookieJar#[]=

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    372       # Sets the cookie named +name+. The second argument may be the cookie's
    373       # value or a hash of options as documented above.
    374       def []=(name, options)
    375         if options.is_a?(Hash)
    376           options.symbolize_keys!
    377           value = options[:value]
    378         else
    379           value = options
    380           options = { value: value }
    381         end
    382 
    383         handle_options(options)
    384 
    385         if @cookies[name.to_s] != value || options[:expires]
    386           @cookies[name.to_s] = value
    387           @set_cookies[name.to_s] = options
    388           @delete_cookies.delete(name.to_s)
    389         end
    390 
    391         value
    392       end

ActionDispatch::Cookies::CookieJar#[]=の中身はこんな感じ。

一行ずつ

actionpack/lib/action_dispatch/middleware/cookies.rb
    374       def []=(name, options)

これを定義すると、

[:name] = options

という形で値を代入できるようになります。

それではここで、Railsアプリからcookies[]= に渡せる値を確認しましょう?

Railsガイド

cookie[:hoge] = "fuga"のように文字列を渡すことが多いと思いますが、オプションを細かく設定する場合、右辺にはハッシュを渡すこともできます。

cookies[:hoge] = {
  value:      'Cookieの値',
  expires:    'Cookieの有効期限',
  path:       'Cookieが適用されるパス(デフォルトは/)',
  domain:     'Cookieが適用されるドメイン',
  tld_length: 'domain: :allの時、TLDの一部として解釈される短い(3文字以下の)ドメインを使用するときに、TLDの長さを明示的に設定',
  secure:     '暗号化通信のみを有効化(デフォルトはfalse)',
  httponly:   'スクリプト経由もしくはHTTP通信のみを有効化(デフォルトはfalse)'
}

これを踏まえて、引き続き処理を読んでいきます。

①引数optionsがハッシュの場合

actionpack/lib/action_dispatch/middleware/cookies.rb
    375         if options.is_a?(Hash)
    376           options.symbolize_keys!
    377           value = options[:value]
    378         else

cookies[:hoge] = {"value" => "fuga}の場合、symbolize_keys!{:value => "fuga"}に変換。

変数value"fuga"を代入。

②引数optionsがハッシュ以外の場合

actionpack/lib/action_dispatch/middleware/cookies.rb
    378         else
    379           value = options
    380           options = { value: value }
    381         end

cookies[:hoge] = "fuga"の場合、変数value"fuga"を代入。

options{value: "fuga"}を再代入。

今の状態

①②いずれの場合も、

  • options{value: "fuga"}
  • valuefuga

となっている。

つづき

actionpack/lib/action_dispatch/middleware/cookies.rb
    383         handle_options(options)

同じクラスのhandle_options(options)を呼びます。

ActionDispatch::Cookies::CookieJar#handle_options

handle_optionsoptionsをいい感じにするためのメソッドです。

actionpack/lib/action_dispatch/middleware/cookies.rb
    350       def handle_options(options) # :nodoc:
    351         if options[:expires].respond_to?(:from_now)
    352           options[:expires] = options[:expires].from_now
    353         end
    354 
    355         options[:path] ||= "/"
    356 
    357         if options[:domain] == :all || options[:domain] == "all"
    358           # If there is a provided tld length then we use it otherwise default domain regexp.
    359           domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
    360 
    361           # If host is not ip and matches domain regexp.
    362           # (ip confirms to domain regexp so we explicitly check for ip)
    363           options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
    364             ".#{$&}"
    365           end
    366         elsif options[:domain].is_a? Array
    367           # If host matches one of the supplied domains without a dot in front of it.
    368           options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
    369         end
    370       end

中身はこんな感じ。

一行ずつ

actionpack/lib/action_dispatch/middleware/cookies.rb
    351         if options[:expires].respond_to?(:from_now)
    352           options[:expires] = options[:expires].from_now
    353         end

cookiesメソッドに有効期限を設定した場合(例:1.week)、from_nowで具体的な日時に変換してoptions[:expires]に再代入。
from_nowsinceのエイリアス。

actionpack/lib/action_dispatch/middleware/cookies.rb
    355         options[:path] ||= "/"

cookiesメソッドにパスが指定されていない場合、ルートパスを設定(デフォルト値として)

actionpack/lib/action_dispatch/middleware/cookies.rb
    357         if options[:domain] == :all || options[:domain] == "all"
    358           # If there is a provided tld length then we use it otherwise default domain regexp.
    359           domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
    360 
    361           # If host is not ip and matches domain regexp.
    362           # (ip confirms to domain regexp so we explicitly check for ip)
    363           options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
    364             ".#{$&}"
    365           end
    366         elsif options[:domain].is_a? Array
    367           # If host matches one of the supplied domains without a dot in front of it.
    368           options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
    369         end
    370       end

誰か正規表現に強い方…………

  • [:domain]オプションが:allor"all"の場合
    • [:tld_length]が指定されている場合は/([^.]+\.?){#{options[:tld_length]}}$/
    • そうでない場合はDOMAIN_REGEXP

を変数domain_regexpに代入
(※DOMAIN_REGEXP/[^.]*\.([^.]*|..\...|...\...)$/)

リクエストメッセージのホストが/^[\d.]+$/に一致しない場合&&変数domain_regexpに一致する場合はoptions[:domain]".#{$&}"で置き換える

  • [:domain]オプションが配列の場合
    • options[:domain]を、リクエストメッセージのホストが含まれる要素で置き換える

ここでやったこと

  • 有効期限を具体的な日時に変換
  • パスの設定がない場合にデフォルト値を設定
  • ドメインをいい感じに再代入(?)

ActionDispatch::Cookies::CookieJar#[]=のつづき

今の状態

①②いずれの場合も、
- options{value: 'fuga'}
- valuefuga

となっています。

ActionDispatch::Cookies::CookieJar#[]=のつづき

actionpack/lib/action_dispatch/middleware/cookies.rb
    385         if @cookies[name.to_s] != value || options[:expires]
    386           @cookies[name.to_s] = value
    387           @set_cookies[name.to_s] = options
    388           @delete_cookies.delete(name.to_s)
    389         end
    390 
    391         value
    392       end

@cookies["hoge"]のvalueが"fuga"でない場合(つまり値が変わっている場合)、もしくは有効期限の設定がない場合に、

  • @cookies["hoge"]"fuga"を代入
  • @set_cookiesにoption{value: 'fuga'}を代入
  • @delete_cookiesから[:hoge]を削除する

をして、最後に"fuga"を返します。

最後に

trace_locationでログに残っていたのはここまでですが、
先述の通りActionDispatch::Cookiescallメソッドを持ったRackミドルウェアとして実装されており、
Railsアプリケーションから使用されています。

$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies # ←ここ 
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes

このことから、Railsアプリケーションにリクエストがあった際、Rackアプリとして次の処理が呼ばれることがわかります。

actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    637     def initialize(app)
    638       @app = app
    639     end
    640 
    641     def call(env)
    642       request = ActionDispatch::Request.new env
    643 
    644       status, headers, body = @app.call(env)
    645 
    646       if request.have_cookie_jar?
    647         cookie_jar = request.cookie_jar
    648         unless cookie_jar.committed?
    649           cookie_jar.write(headers)
    650           if headers[HTTP_HEADER].respond_to?(:join)
    651             headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
    652           end
    653         end
    654       end
    655 
    656       [status, headers, body]
    657     end

中身は詳しく追っていきませんが、

actionpack/lib/action_dispatch/middleware/cookies.rb
    647         cookie_jar = request.cookie_jar
    648         unless cookie_jar.committed?
    649           cookie_jar.write(headers)
    650           if headers[HTTP_HEADER].respond_to?(:join)
    651             headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
    652           end
    653         end

この辺りでヘッダーへの書き込みが行われていることが確認できました。

今回追うことができなかった処理・残された疑問点

  • "rack.request.cookie_hash"がヘッダーに書き込まれるタイミング
  • Cookies::CookieJar#writeCookies::CookieJar#make_set_cookie_headerから、最終的に呼ばれる::Rack::Utils.add_cookie_to_header(↑の資料では未出)
    • ヘッダーに書き込む文字列の成形を行なっていることが確認できるが、その後実際にヘッダーに書き込まれるタイミング

おつかれさまでした?

参考資料

おまけ

TraceLocationで作成したログは次の通りです(マークダウン形式)
Generated by trace_location at 2019-06-05 20:28:50 +0900


actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12

ActionController::Cookies#cookies
def cookies
  request.cookie_jar
end
# called from /Users/misakishioi/Projects/myapp/app/controllers/blogs_controller.rb:8



actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11

ActionDispatch::Request#cookie_jar
def cookie_jar
  fetch_header("action_dispatch.cookies") do
    self.cookie_jar = Cookies::CookieJar.build(self, cookies)
  end
end
# called from actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:13



rack-2.0.7/lib/rack/request.rb:58

Rack::Request::Env#fetch_header
def fetch_header(name, &block)
  @env.fetch(name, &block)
end
# called from actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:12



actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374

ActionDispatch::Cookies::CookieJar#[]=
def []=(name, options)
  if options.is_a?(Hash)
    options.symbolize_keys!
    value = options[:value]
  else
    value = options
    options = { value: value }
  end

  handle_options(options)

  if @cookies[name.to_s] != value || options[:expires]
    @cookies[name.to_s] = value
    @set_cookies[name.to_s] = options
    @delete_cookies.delete(name.to_s)
  end

  value
end
# called from /Users/misakishioi/Projects/myapp/app/controllers/blogs_controller.rb:8



actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:350

ActionDispatch::Cookies::CookieJar#handle_options
def handle_options(options) # :nodoc:
  if options[:expires].respond_to?(:from_now)
    options[:expires] = options[:expires].from_now
  end

  options[:path] ||= "/"

  if options[:domain] == :all || options[:domain] == "all"
    # If there is a provided tld length then we use it otherwise default domain regexp.
    domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP

    # If host is not ip and matches domain regexp.
    # (ip confirms to domain regexp so we explicitly check for ip)
    options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
      ".#{$&}"
    end
  elsif options[:domain].is_a? Array
    # If host matches one of the supplied domains without a dot in front of it.
    options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
  end
end
# called from actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:383

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

初心者)Twitter クローン課題の苦労した点ピックアップ

1.コメントコミット忘れがち。。

git add .

からの、、

git commit -m 'ここにコメント作成します'

メンターさんにも課題提出時に指摘されたのですが、コミットする粒度が荒い。要はもっとコマメにセーブポイントを作りなさいとのこと。
最初は細かすぎるくらいのイメージでやって徐々に粒度を荒くしていく感じがいいのかもしれない。

例えばこんな感じでやればよかったのかも、、

データベース作成(ファーストコミット)

モデル作成・マイグレーション(コミット)

バリデーション作成(コミット)

コントローラー作成(コミット)

indexページの作成(コミット)

showページの作成(コミット) 

......

2.Confirm画面がややこしい

hidden_fieldというのがピンとこないんだが、
要はフォーム画面なんだけど、入力フォームを生成せずに値のみを保持する機能ということらしいです。
値の表示については下記のPタグで別途出力している。
(Pタグを削除すると値の表示はされないが、hidden_field で値は保持されているので登録は可能)

ただこのままではNewアクションのForm_withはCreateに飛んでしまうので、確認画面に飛ぶようにルーティングし直す必要がある。

ヘルパーを使ってNewとEditの分岐をするのですが、その際はHTMLのFoamタグに必須のAction属性を使って分岐させる。

その際はFoamタグ作成の際にはAction属性(必須)とMethod属性(オプション)の指定を行う。

Action属性
1.formタグに指定する属性で、必ず指定しなければならない
2.フォームの送信ボタンを押して送信されるデータの送信先を指定する


Method属性
1.formタグに指定する属性で、必須ではない
2.送信するときの転送方法を指定する
3.postとgetがある

module BlogsHelper
  def choose_new_or_edit
    if action_name == 'new' || action_name == 'confirm'
      confirm_blogs_path
    elsif action_name == 'edit'
      blog_path
    end
  end
end

3.確認画面から値を保持しつつ「戻る」のがややこしい

hidden_fieldは値を保持しているので、これを使って値をnewのページを送り返している。戻るボタンのform_withはnewアクションを実行リクエストするため、HTTPメソッドがgetになるので注意してください。

さっき出てきたMethod属性-getを活用しています。
それはformタグのmethodをgetに指定すると、入力したフォーム内容のデータがURIにくっついて送信されるから。
ちなみにmethodをpostに指定すると、入力したフォーム内容はURIとは別の場所に保管されてデータが送信される。
入力したフォーム内容のデータは外側からの表示では見ることができないので安全性が高いのが利点となる。

<%= form_with(model: @blog, url: new_blog_path, local: true, method: 'get') do |form| %>
  <%= form.hidden_field :title %>
  <%= form.hidden_field :content %>
  <%= form.submit "戻る" %>
<% end %>

(おまけ) 何故、formに"local: true"を入れるのか

検索から。。
local: trueがない場合、Rails 5ではAjaxによる送信という意味になります。ふつうにHTMLとしてフォームを送信する場合にlocal:trueが必要となります。

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

データベースに保存されている時刻とビューに表示される時刻が違う

問題:データベースの値とビューに表示される時刻が違う

仮説

1.DateTime.parse(string)で生成されたDateTimeクラスのインスタンスは協定世界時になる
2.datetime型のカラムに保存した時にJST時間に修正される

解決

1.DateTime.parse(string)で生成されるインスタンスはUTCになるは正しかったぽい参考記事

parseはUTCのタイムゾーンを使う。(環境変数のタイムゾーンを無視する)
l DateTime.parse(str)
=> 2015/01/01 00:00:00, +00:00, DateTime

2.MysqlのDATETIME型はActiveSupport::TimeWithZoneクラスであるので、レコードを保存する時に内部でこんな感じの処理が行われる、だと思う。

time = DateTime.parse(string)
=> Tue, 09 Jul 2019 12:00:00 +0000

Time.zone.parse(time.to_s)
=> Tue, 09 Jul 2019 21:00:00 JST +09:00

Time.zone.parse(time.to_s).class
=> ActiveSupport::TimeWithZone

Time.zone.parse(time.to_s).strftime("%Y年%-m月%-d日 %-H時%-M分")
=> "2019年7月9日 21時0分"
> string
=> "201907091200"

> Time.zone.parse(string)
=> Tue, 09 Jul 2019 12:00:00 JST +09:00

#DateTime.parseした場合
> DateTime.parse(string)
=> Tue, 09 Jul 2019 12:00:00 +0000 //UTCで返ってくる

> @event.deadline_date
=> Tue, 09 Jul 2019 21:00:00 JST +09:00

> @event.deadline_date.class
=> ActiveSupport::TimeWithZone

#Time.parseした場合
> Time.parse(string)
=> 2019-07-09 12:00:00 +0900 //JST(アプリケーションで設定したタイムゾーン)で返ってくる

> @event.deadline_date
=> Tue, 09 Jul 2019 12:00:00 JST +09:00

> @event.deadline_date.class
=> ActiveSupport::TimeWithZone

仮説

データベースに保存される時にDateTimeクラスからTimeWithZoneクラスに変換される

> string
=> "201907060600"

> DateTime.parse(string)
=> Sat, 06 Jul 2019 06:00:00 +0000

> Time.zone.parse(DateTime.parse(string).to_s)
=> Sat, 06 Jul 2019 15:00:00 JST +09:00

でも違うのか?

DateTimeクラスのデータをデータベースに保存するときに保存先のデータ型に変換されて保存されると思っていたけれど、データベースには 2019-07-06 06:00:00 で格納されていた。(画像3行目)
スクリーンショット 2019-07-07 10.41.24.png
でも引っ張り出すとTimeWithZone型でJSTの時刻になっていた。

> @event.deadline_date
=> Sat, 06 Jul 2019 15:00:00 JST +09:00

> @event.deadline_date.class
=> ActiveSupport::TimeWithZone

> @event.deadline_date.strftime("%Y年%-m月%-d日 %-H時%-M分")
=> "2019年7月6日 15時0分"

解決

データベースに保存された時には実際にデータ型への変換が行われるが、UTCの時刻で表示されている
アプリケーション側のタイムゾーンをtokyoつまりJSTに設定している場合はTime.parseを使ったほうがいいのかもしれない

ご指摘ください

7月からエンジニアになりました
間違っているところがありましたらご指摘ください
時間ある時に修正します
何卒よろしくお願いします

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

Go言語とRubyの比較【入門】①

はじめに

記念すべき初めての投稿です。初心者エンジニアです。
仕事でどちらも使用するので
Go言語とRubyの基本的なところをまとめてみようと思います。

Hello World!

Rubyの場合

helloworld.rb
puts "Hello World!"
#=> "Hello World!"

Goの場合

helloworld.go
package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}
//=> "Hello World!"

変数宣言

Rubyの場合

variable.rb
num = 1
name = "test"
pi = 3.14
t = true
f = false

puts num, name, pi, t, f

Ruby実行結果

1
test
3.14
true
false

Goの場合

variable.go
package main

import "fmt"

//基本
var (
    i    int     = 1
    s    string  = "test"
    f64  float64 = 3.14
    t, f bool    = true, false
)

//省略型
func test() {
    xi := 1
    xs := "test"
    xf64 := 3.14
    xt, xf := true, false
    fmt.Println(xi, xs, xf64, xt, xf)
}

func main() {
    fmt.Println(i, s, f64, t, f)
    test()
}

Go実行結果

1 test 3.14 true false
1 test 3.14 true false

定数

Rubyの場合

一度定義した定数に再代入を行うと警告は出るが値は変わる。※要注意
変更を防ぐためにfreezeメソッドを使う方法はまたの機会に。

const.rb
# 定数は必ず大文字で始める必要あり
COUNTRY = "JAPAN"

# 破壊的変更
COUNTRY.downcase!

# 変更されてしまう
COUNTRY #=> "japan"

# 再代入
COUNTRY = "SPAIN"

# 警告発生するが値は変更されてしまう

warning: already initialized constant COUNTRY
warning: previous definition of COUNTRY was here

COUNTRY #=> "SPAIN"

Goの場合

Rubyと違い、定数の定義後に値を代入することはできません。

const.go
package main

import "fmt"

//constキーワードを使用して定数を定義
const Pi = 3.14

//まとめて定義
const A, B, C int = 0, 1, 2

const (
    Username = "test_user"
    Password = "test_pass"
)

func main() {
    fmt.Println(Pi)
    fmt.Println(A, B, C)
    fmt.Println(Username, Password)
    fmt.Printf("%T %T %T %T %T %T", Pi, A, B, C, Username, Password) //型確認
}

Go実行結果

3.14
0 1 2
test_user test_pass
float64 int int int string string
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsで指定したディレクトリ以下のファイルを取得する時の解決方法

Railsで指定したディレクトリ以下のファイルを取得する時につまずいたので共有します。

環境

バージョン

$ rails -v
Rails 5.2.3
$ ruby -v
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-darwin18]

ディレクトリ構成(該当部分のみ)

app/commands/command.rb
app/commands/command/hoge_command.rb
app/commands/command/fuga_command.rb
app/commands/command/piyo_command.rb

ソースコード

# app/commands/command.rb
class Command
  def initialize(command_name)
    files_abs_pass = Dir[File.expand_path("#{Rails.root}/app/commands/command/", __FILE__) << '/*.rb']

    files_abs_pass.each { |f| puts f }
    # /Users/user/work/rails/rails_app/app/commands/command/hoge_command.rb
    # /Users/user/work/rails/rails_app/app/commands/command/fuga_command.rb
    # /Users/user/work/rails/rails_app/app/commands/command/piyo_command.rb
  end
end
# app/commands/command/hoge_command.rb
class HogeCommand < Command
end

当初はこのページを参考にしていたのですが、ファイル一覧を取得できなかったので変えました。

# 掲載元のコード
Dir[File.expand_path('../commands', __FILE__) << '/*.rb'].each do |file|
  require file
end

JavaのJSPでも同じような事象に悩まされていたので、解決方法はこれでいいんじゃないかって思えましたw(uriの指定)

もっとスマートに書けるはずなので、気が向いたらあとがきにでも書きます。。。(コメント頂けたらとても嬉しいです...!)

引用元

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