20191218のRubyに関する記事は22件です。

Railsで特定のURLからのリクエストを許可してiframeを表示させたい、、、

やりたいこと

Railsでiframeからのリクエストを許可して外部のサイトで表示させたい、、、
でもすべて許可するとなにかと怖いので特定のURLに絞りたい、、、

ということで

Railsでは

response.headers['X-Frame-Options'] = 'SAMEORIGIN'

が設定されていて自身のドメインしかiframeで表示出来ないそう

今回は特定のURLのみ許可したいので以下のように実装

class HogeController < ApplicationController
    after_action :allow_iframe, only: [:hoge]

    def hoge
        render "hoges/index", layout: false
    end

    private
    def allow_iframe
        url = "https://hoge.com"
        response.headers['X-Frame-Options'] = "ALLOW-FROM #{url}"
        response.headers['Content-Security-Policy'] = "frame-ancestors #{url}"
    end
end

注意点

なんと皆がよく使ってるであろうChrome、Safariなど多くのブラウザが
X-Frame-Options ALLOW-FROM
に対応していません1

なので合わせて
Content-Security-Policy frame-ancestors
も記述してやりましょう2

調べてみた感じIE以外対応してたので安心です3

効いてるか確認

Chrome DevToolsのNetwork パネルで確認できます
C__でなにができるの?おススメ案件!___ITフリーランスエンジニアのためのコラム【at-engineer】.png


  1. X-Frame-Options ブラウザ対応表 

  2. X-Frame-Options ALLOW-FROMだけ設定してしまうと未対応ブラウザですべてのURLを許可する状態になってしまいます 

  3. Content-Security-Policyブラウザ対応表 

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

新規アプリケーションをGithubで管理する(Rails)

はじめに

Railsを使ってプログラミングの学習をしているのですが、新規アプリケーションを作成する際、Githubで管理する方法を忘れてしまうので、定着させる意味でも残したいと思います。

環境

OS: Mac
Rails 5.0.7.2
Ruby 2.5.1
MySQL
Github Desktop

手順

1,新規アプリケーションを作成

ターミナル
$ cd myapp #作成したいディレクトリへ移動
$ rails _5.0.7.2_ new my-blog -d mysql #Railsのバージョンを指定、DBはMySQLを指定する。
$ cd my-blog #作成したアプリケーションのディレクトリへ移動
$ rails db:create #データベース作成

2,Github Desktopからローカルリポジトリにする

スクリーンショット 2019-12-18 1.24.07.png
Github Desktop左上のCurrent Repositoryをクリック→AddAdd Existing Repositoryを選択。

スクリーンショット 2019-12-18 1.29.51.png
選択画面が出てくるので、作成したアプリを選択。

Add Repositoryできない場合
ターミナルにて、作成したアプリのリポジトリ→git initのコマンドを打てば解消されます。

スクリーンショット 2019-12-18 1.32.55.png
左下から最初のコミットをする。スクリーンショット 2019-12-18 1.34.44.png
Publish repositoryをクリック。
スクリーンショット 2019-12-18 1.35.12.png
公開してもいいならKeep this code privateのチェックを外し、Publish Repositoryをクリック。

3,Githubのリモートリポジトリが作成される

スクリーンショット 2019-12-18 1.40.05.png
GithubのYour Repositoriesに作成したアプリが登録されていれば完了です。

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

オブジェクト指向設計を実践するためのまとめ

Zeals Advent Calendar 2019 の19日目の記事です。

サーバサイドエンジニアの小寺です。私がまだ初学者に近かった頃、「もっと早く知っておけばよかった!」と心の底から思ったオブジェクト指向設計についての知識を書き残しておこうと思います。

今回は、実務で求められる「保守性の高い読みやすいコード」を実践するために
オブジェクト指向設計実践ガイドを学習し、理解できたところを自分なりに要点をまとめ・メモし、備忘録としたものです。
(※4章までの内容が理解できれば十分実践していけると思いますので4章までしかまとめてないです)

参考書籍

オブジェクト指向設計実践ガイド
本書のサンプルコードを引用しています。

対象

  • オブジェクト指向についてふんわり理解している人
  • 責務がどうのこうのといったくだりをふんわり理解してる人
  • オブジェクト指向設計を理解してステップアップしたい初学者

2章 単一責任を意識する

  • なぜ単一責任が必要なのか
    • 再利用したい
    • 再利用できると未来の予期せぬ変更に対応できる
    • 悪いコードを増殖させたくない

単一責任の見極め方

  • 役割を聞く
# 「オブジェクト指向設計実践ガイド」 p41
class Gear
  attr_reader :chainring, :cog, :rim, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @rim = rim
    @tire = tire
  end
  :
  def gear_inches
    ratio * (rim + (tire * 2))
  end
end
gear = Gear.new(52, 11, 26, 1.5)
gear.ratio
# ギアに対してギア比を聞くのは良い
gear.gear_inches
# ギアに対してgear_inchesを聞くのはしっくりこないような気がする
gear.tire
# ギアに対してタイヤのサイズを聞くのはもっとおかしい

インスタンス変数の隠蔽

# 「オブジェクト指向設計実践ガイド」 p46
class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f  # <-- 破滅への道
  end
end

アクセサメソッド(attr_reader)で隠す

# 「オブジェクト指向設計実践ガイド」 p46
class Gear
  attr_reader :chainring, :cog # <--
  def initialize
  :
  def ratio
    chainring / cog.to_f  # <--
  end
end

実装的には以下と同じ

def cog
  @cog
end

実際に得ている恩恵としては未来に複雑な実装が来た時に変更が簡単

def cog
  if 条件
    @cog * 何かの計算
  else
    (@cog * 何かの計算) + 複雑な計算
  end
end

データ構造の隠蔽

# 「オブジェクト指向設計実践ガイド」 p48
class ObscuringReferences
  attr_reader :data
  def initialize(data)
    @data = data
  end

  def diameters
    # 0はリム, 1はタイヤ  <-- 何のデータがどこにあるか知っている状態
    # 配列の構造が変わった時などに怖い
    data.collect {|cell|
      cell[0] + (cell[1] * 2)
    }
  end
end

# 以下のデータが必要になる
@data = [[622, 20],[622, 23],...]

Structクラスを使って構造を包み隠す

# 「オブジェクト指向設計実践ガイド」 p50
class ObscuringReferences
  attr_reader :wheels
  def initialize(data)
    @wheels = wheelify(data)
  end

  def diameters
    # wheelが rim と tire を持ってることだけ知っていればいい
    wheels.collect {|wheel|
      wheel.rim + (wheel.tire * 2)
    }
  end
  # これで誰でも wheel に rim/tire を送れる

  Wheel = Struct.new(:rim, :tire)
  def wheelify(data)
    data.collect {|cell|
      cell[0] + (cell[1] * 2)
    }
  end
end

メソッドから余計な責任を抽出する

メソッドもクラスと同じく単一の責任を持つべき(再利用が簡単になるので)
先ほどの diameters を見てみる

# 「オブジェクト指向設計実践ガイド」 p52
class ObscuringReferences
  :
  def diameters
    # wheelsを繰り返し処理するのと、それぞれの直径を計算する
    # といった2つの責任を持っている
    wheels.collect {|wheel|
      wheel.rim + (wheel.tire * 2)
    }
  end
  :
end

上記のメソッドを簡単に変更できるようにしていく

# 「オブジェクト指向設計実践ガイド」 p52
class ObscuringReferences
  :
  def diameters
    # 直径を計算する処理を別にした
    wheels.collect {|wheel| diamete(wheel) }
  end

  # diameter を呼べるようになった(他で再利用可能になった)
  def diameter(wheel)
    wheel.rim + (wheel.tire * 2)
  end
  :
end

上記はよくある責任が複数あるわかりやすい例。
大抵はこれほど明確ではない。先ほどのGearクラスを思い出してみる。

# 「オブジェクト指向設計実践ガイド」 p53
class Gear
  :
  # 何か不確定でのちにトラブルを起こしそうな気がする
  # このメソッドが2つ以上の責任を持ってしまっている
  def gear_inches
    ratio * rim + (tire * 2)
  end
  :
end

gear_inches に隠れている直径の計算を抽出してみる

# 「オブジェクト指向設計実践ガイド」 p53
class Gear
  :
  # Gearがgear_inchesの計算するのはよし
  def gear_inches
    ratio * diameter
  end

  # しかし、車輪の直径の計算までするのはおかしい
  def diameter
    rim + (tire * 2)
  end
  :
end

といったように、小さなリファクタリングではあるが Gear が持つべきでない責任が明らかになりました。

このように、あらゆるものを単一責任にしていくことで

  • 隠蔽された性質を明らかにする
    • 上記の例だと 「Gear は diameterを持つべきではない」
  • コメントをする必要がない
    • メソッドがコメントの役割を果たす
  • 再利用を促進する
    • 他のプログラマーは複製ではなく再利用をするようになる
  • 他のクラスへの移動が簡単
    • 小さなメソッドは簡単に動かすことができる

といった恩恵が受けれます

クラス内の余計な責任を隔離する

一旦全てを単一責任するとクラスのスコープが明白になってきます。
GearクラスにはいくつかのWheel(車輪)のような振る舞いが隠れていました。
ここで、新しくクラスを作ってもいいのですがそうなると変更のコストが大きいかもしれないのでコストを最小限にしつつGearの単一責任を保つようにします。

# 「オブジェクト指向設計実践ガイド」 p55
class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @wheel = Wheel.new(rim, tire)
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end

  # 隠れていた Wheel のような振る舞いを手に入れた
  # 書籍ではこの後追加要望があったタイミングで新しいクラスを定義する
  # 「あとで」決定できる力を取っておくことはアプリケーション開発において重要
  Wheel = Struct.new(:rim, :tire) do
    def diameter
      rim + (tire * 2)
    end
  end
end

本書では、新しくアプリケーションに車輪に関する追加機能の要望が来たタイミングで Wheel クラスを新しく定義するよう書かれています。
アプリケーション開発において未来を予測することは困難なのでできる限り「あとで」決定できる力を残しておくのは非常に重要なことである

3章 依存関係を管理する

依存関係を認識する

オブジェクトが次のものを知っている時、オブジェクト間には依存がある

  • 他のクラスの名前を知っている
    • GearはWheelというクラスが存在すると予想している
  • self以外のメッセージの名前
    • GearはWheelがdiameterに応答できると知っている
  • メッセージが要求する引数
    • Gearは Wheel.newするときに rim とtireが必要なことを知っている
  • 引数の順番
    • 最初が rim で 2番目が tire だと知っている
# 「オブジェクト指向設計実践ガイド」 p60
class Gear
  attr_reader :chainring, :cog, :rim, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    # Wheel.newするときに rim とtireが必要なことを知っている
    @rim = rim
    @tire = tire
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    # Wheelというクラスが存在すると予想している
    # Wheelがdiameterに応答できると知っている
    # 最初が rim で 2番目が tire だと知っている
    ratio * Wheel.new(rim, tire).diameter
  end
  :
end

class Wheel
  attr_reader :rim, :tire
  def initialize(rim, tire)
    @rim = rim
    @tire = tire
  end

  def diameter
    rim + (tire * 2)
  end
  :
end

依存が発生している時、依存が強固になりすぎると2つのオブジェクトは変更することが難しくなってくるので2つのオブジェクトがあたかも1つのオブジェクトとして振る舞うようになってしまう。

疎結合なコードを書く

依存を減らすための技法を紹介する

依存オブジェクトの注入

クラス内でインスタンスを生成するのではなく diameterを知っているオブジェクトを受け取るようにしておく

# 「オブジェクト指向設計実践ガイド」 p66
class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, wheel)
    @chainring = chainring
    @cog = cog
    @wheel = wheel
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end
  :
end

# Gearはdiameterを知っているオブジェクトを要求する
Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches

依存を隔離する

  • インスタンス変数の作成の分離
    • 制約が厳しく、オブジェクトを注入できないような場合
    • クラス内で分離するしか方法がないとき
# 「オブジェクト指向設計実践ガイド」 p68
class Gear
  attr_reader :chainring, :cog, :rim, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    # Gearはまだ知りすぎているが、gear_inchesの依存が減っている
    @rim = rim
    @tire = tire
  end
  :
  def gear_inches
    ratio * wheel.diameter
  end

  # wheel が呼ばれるまでインスタンスは作成されない
  def wheel
    @wheel || Wheel.new(rim, tire)
  end
end
  • 脆い外部メッセージを隔離する
    • 外部への参照がクラスに埋め込まれていて、どれが変更されやすい時
# 「オブジェクト指向設計実践ガイド」 p70
def gear_inches
  # 恐ろしい計算
  foo = some_intermedicate_result * wheel.diameter
  # 恐ろしい計算
end

上記のように複雑な状況になった時にメソッドの中に依存が隠れてしまう。

そうした場合以下のように脆い部分を隔離する

# 「オブジェクト指向設計実践ガイド」 p71
def gear_inches
  # 恐ろしい計算
  foo = some_intermedicate_result * diameter
  # 恐ろしい計算
end

def diameter
  # もし、Wheelがdiameterに変更加えた場合副作用はここだけになる
  wheel.diameter
end

引数の順番への依存を取り除く

  • 初期化の際にハッシュ使う
# 「オブジェクト指向設計実践ガイド」 p73
class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(args)
    @chainring = args[:chainring]
    @cog       = args[:cog]
    @wheel     = args[:wheel]
  end
  :
end

# 冗長にはなったが、引数の順番を知っていなくても大丈夫になった
# ハッシュのkey名に依存はしているが、順番よりは健康的
Gear.new(
  chanring: 52,
  cog:      11,
  wheel:    Wheel.new(26, 1.5)
).gear_inches

安定性の高い引数をいくつか受け取って、安定性の低い引数はオプション引数で受け取るといった手法が多く用いられる

  • 明示的にデフォルト値を設定する
# 「オブジェクト指向設計実践ガイド」 p74
def initialize(args)
  # 真偽値以外の場合はシンプルに || を使える
  @chainring = args[:chainring] || 40
  @cog       = args[:cog] || 18
  @wheel     = args[:wheel]
end

# 真偽値を使う or nilを設定する場合
def initialize(args)
  @chainring = args.fetch(:chainring, 40)
  @cog       = args.fetch(:cog, 40)
  @wheel     = args[:wheel]
end
  • 複数のパラメーターを用いた初期化を隔離する
    • もし、引数を簡単に変更できないような状況下にあった場合ラッパークラスを定義して包み隠すようにする
# 「オブジェクト指向設計実践ガイド」 p77

# Gearがもし外部インターフェースの一部の場合(このクラスを変更できない場合)
module SomeFramework
  class Gear
    attr_reader :chainring, :cog, :wheel
    def initialize(args)
      @chainring = args[:chainring]
      @cog       = args[:cog]
      @wheel     = args[:wheel]
    end
    :
  end
end

# 外部のインタフェースをラップし、自信を変更から守っている
# 他のオブジェクトを生成することが目的のオブジェクトはファクトリーと呼ぶ
module GearWrapper
  def self.gear(args)
    SomeFramework::Gear.new(
      args[:chainring],
      args[:cog],
      args[:wheel]
    )
  end
end

GearWrapper.new(
  chanring: 52,
  cog:      11,
  wheel:    Wheel.new(26, 1.5)
).gear_inches

このテクニックは自分で変更がきかない外部のインターフェースに依存する場合に適している。

依存方向の管理

「自分より変更が少ないものに依存すべし」

4章 柔軟なインターフェースを作る

パブリックインターフェースとは

クラスのパブリックインターフェースを作り上げるメソッドは以下のような特性を備えている

  • クラスの主要な責任を明らかにする
  • 外部から実行されることが想定される
  • 気まぐれに変更されない
  • 他者がそこに依存しても安全
  • テストで完全に文書化されている

パブリックインターフェースを見つける

「アプリケーション例: 自転車旅行会社」
ユースケースとして
「参加者は適切な難易度の、特定の日付の、自転車を借りられる旅行の一覧をみたい」

見当をつけるためにシーケンス図を使う

オブジェクト間でやり取りされるメッセージを気軽に実験することができる

SS 404.png

ここで起きる疑問
「Tripが利用可能な自転車まで調べなくてもいいんじゃないか?」

シーケンス図を描くことによって
「このオブジェクトが〇〇という責任を負うべきなのだろうか?」と疑問が湧くようになる

ある旅行に対して自転車が利用可能かどうかTripクラスが見つけ出すべきでない場合、
Bycycleクラスがありそう。

SS 405.png

  • Tripはsuiable_tripsに責任があり
  • Bycycleはsuitable_bycycle に責任を持つ

変更後は Tripから余計な責任は取り除けたものの Customer に移しただけ
変更後は Customer が何を望むのかと他のオブジェクトがどのようにそれを準備するのかまで知ってしまっている

「どのように」を伝えるのではなく「何を」を頼む

新たなユースケース
「旅行が開始されるためには使われる自転車が全て整備されていることを確実にする」

SS 406.png

TripはMechanicがどのように整備するのか(
clean_bicycleして
pump_tiresして
lube_chainして
check_brakesするという手順
)を知ってしまっている。

Mechanicが新たな整備手順を増やした時は、Tripも変更しなければならない

以下は対案

SS 407.png

Tripにあった責任をほとんどMechanicに渡している
自転車の準備することに関して「どのように」はMechanicの責任になった
TripはMechanicにどんな改善があろうともprepare_bicycleから正しい振る舞いを得ることができる

上記のようにMechanic と Trip の会話が 「どのように」から「何を」に変わった副作用として
Mechanicのパブリックインターフェースのサイズが小さくなった

パブリックインターフェースが小さいということは他のところから依存されるメソッドがわずかしかないことを意味している

コンテキストの独立を模索

旅行の準備には「いつでも」自転車の準備が求められるためTripは「常に」prepare_bicycleメッセージを自身のMechanicへ送らなければならない

SS 408.png

Tripが旅行が準備されることをMechanicに伝え、Mechanicは準備にbicyclesが必要なのでTripにコールバックをし、自転車の整備を行います。

こうして、整備士がどのように自転車を準備するかはMechanicクラスに隔離されました。

オブジェクトを見つけるためにメッセージを使う

SS 405.png

Tripが知りすぎていたのを改善したが、今度はCustomerが知りすぎている。
上記のアプリケーションは要件を満たす新たなオブジェクトを必要としていることがなんとなく見えてきた。

SS 409.png

新たなTripFinderは安定したパブリックインターフェースを提供し、複雑な内部に関しては隠している

シーケンス図便利ー

まとめ

単一責任を意識しながらコードを書けるようになりましょう

  • そのために常にメソッドの役割をメソッド自身に問い続ける
  • 抽出できるものはできる限り抽出しておくと未来に変更が簡単になる

クラスの中で依存関係があることを認識する

  • 状況に応じて疎結合にするための技法を使い分ける

オブジェクト間で交わされるメッセージを中心にアプリケーションを設計する

  • 議論を進める時にはシーケンス図が有効
  • どのようにではなくオブジェクトが「何を」要求するかに注目する

おわり

  • 書籍の中に重要な説明がたくさんあるので本読みましょう(オブジェクト指向設計実践ガイド
    • 今回はあくまでコードベースで要点まとめただけなので
  • 柔軟に進化し続けましょう

明日は20日目の @neuneu39 の番です。お楽しみに!!!

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

[Rails6]ActionTextの入力フォームが伸びて困る

はじめに

就活のポートフォリオサイトとしてAsobiというWebサイトを作成しました。(QiitaにAsobiに関しての記事を書いています。就活用ポートフォリオとしてWebサービス「Asobi」を作りました。)
このサイトの中でRails6から新しく導入されたActionTextを使っているのですが、入力フォームが行数に応じて伸びてしまいます。

今回はそんなActionTextの入力フォームをいい感じにするためにやったことをご紹介します。

実行環境

  • Ruby 2.6.5
  • Rails 6.0.2

ActionTextとは

Rails6から実装されたリッチテキストコンテンツと編集機能を導入する機能です。

ActionTextのインストールと導入したいモデルとカラムの用意を行い、少しコードを書くだけでブログのようなリッチテキストエディタが導入できます。

actiontext_test.gif

Ajaxを利用した画像のアップロードも実装されており、アップロードされた画像はActiveStorageを利用して保存されます。

actiontext_pic.gif

今回ActionTextの導入に関しては割愛させていただきます。
導入に関しては下記の記事が参考になるかと思います。

Rails6新機能 ActionText使用方法
Rails 6 と Action text を使ってみる - もふもふ技術部

伸びて困る

ActionTextで生成されるリッチテキストエディタの入力フォームは、行数に応じて高さが伸びるようになっています。

デフォルトの挙動

actiontext_default.gif

これをQiitaやはてなブログのような、はみ出た部分をスクロールバーで表示するように実装します。

こんな風な挙動にしたい

actiontext_custom.gif

実装

Rails側でこれを実装する方法を見つけられなかったため、CSSで実装します。

Trixについて

実装の前に、ActionTextに含まれるTrixについて説明します。
Trixとはリッチテキストエディタを実装するJavascriptのライブラリです。(ちなみに開発元はRuby on Railsと同じBasecampです)
Trixのドキュメントを見てみると、

Place an empty <trix-editor></trix-editor> tag on the page. Trix will automatically insert a separate <trix-toolbar> before the editor.
Like an HTML <textarea>, <trix-editor> accepts autofocus and placeholder attributes. Unlike a <textarea>, <trix-editor> automatically expands vertically to fit its contents.

とのことで、ページのTrixのエディタを入れたい場所にtrix-editorタグを書き込むことで使えるようになります。

ActionTextの場合、erbファイルに

<%= form.rich_text_area :body %>

と記述することでtrix-editorタグが生成されます。
そしてリッチテキストエリアへの入力はtrix-editorタグの子要素にDOMとなって反映されていきます。

actiontext_dom.gif

なので、入力フォームのスタイルは親要素であるtrix-editorタグのCSSを記述することで調整できそうです。

CSSの記述

入力フォームの高さを固定するためにmin-heightmax-heightを、そして入力フォームからはみ出した部分はoverflow-y: auto;を設定することでスクロールバーで表示できるようにします。

以下のCSSを記述します。(actiontext.scss/app/assets/app/javascriptに生成されていると思うので、そこに追記することをオススメします)

trix-editor {
  min-height: 20em;
  max-height: 20em;
  overflow-y: auto;
}

以上で目指す挙動の入力フォームになっていると思います。

まとめ

ActionTextは新しい機能のため中々情報が出てきません。もしかしたらActionTextの設定ファイルか何かでうまいことやれるかもしれないです。

何か直すべき点、間違った記述があればコメント等で指摘していただければと思います。

参考文献

Trix
https://railsguides.jp/action_text_overview.html

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

未経験が受託企業に入って半年経ったので、学んだスキル全部書いてみる

はじめに 

今年2019年6月にエンジニアとして晴れてキャリアをスタートさせました。
それから約半年間で本当にいろいろなことを学ばせて頂いたので、私ごとですが簡単に学んだことをまとめます。

もちろんマスターなんてしておらず、全てが勉強中なうえ、なんならちょっとかじっただけのものまで書いてますので悪しからず。

一概に未経験といっても、自社開発/受託企業/SES企業、大手/メガベンチャー/スタートアップ、元IT業界/全くの素人、普段からパソコン触ってた/触ってない等いろいろなキャリアの始め方があると思うので、平均的な成長度合いなのかどうかは不明ですが、ほんの一例としてご参考ください。

■簡単な経歴

・良くも悪くもない普通の大卒
 パソコンスキルはitunesで音楽入れる程度
・某アパレルチェーン企業で3年ほど勤務
 内2年管理職
 パソコンスキルはWordで毎週報告書を書く程度
・2018/12〜  Mac購入
       某オンラインスクールで勉強開始
・2019/3〜 独学開始、都内もくもく会に週1参加、Menta契約、アプリ作成等
・2019/6〜 就職
・2019/12 現在

0ヶ月〜半年で学んだこと

1)プログラミング技術

Ruby/Rails

ほぼ毎日触っていました。

基本的には既存のコードや他PJのコードをマネて書くことが多いですが、必ず意味を理解しながら次へ繋げています。
ただやはりまだまだ分からないことが多く、日々痛感しています。

ここでいろいろ書蹴たらいいのですがキリがなさそうなので、また別の記事で書きます。

HTML/CSS

Railsに合わせてHTMLはslimで書いていました。

特にCSSの方は奥深すぎてほんとナメてました。めちゃくちゃ難しいです。
新システム開発時にいかに大勢のユーザーに"初めから"良い印象を持ってもらえるかと考えた時に、画面作成でかなり手こずりました。

SQL

本番データをよく触らせて貰える案件だったので、抽出しなければならないことが多くありました。

入社前は全く勉強できてなかったのですが、基礎的なところからサブクエリの書き方、inner join left outer join concatあたりに触れ、
加えてlimitをつけずクエリ重すぎて本番が落ちる等も経験できたので、とても良い経験()になりました。

DB

上記と同じく、よく触らせて貰えました。
案件JOIN当初、railsdbから大事なレコードをそのまま物理削除してしまい、その直後5分後くらいに先方から怒りの電話がかかってきたのは良い思い出()です。

mysqlコマンドやdump、トランザクションの仕組みなど知ることができました。

Git

入社前からもちろん触っていましたが、git mergegit clone等は正直あまりよく分かっていませんでした。

現在でもまだそこまで幅広く扱えてないですが、
それでもgit add前にgit statusgit diffで変更内容をちゃんと確認する習慣付けや、コンフリクト発生時の解消方法、git fetch git stash git reset --hard HEAD^あたりを日常的に使えるようになりました。

IDE操作

cloud9で開発していました(これ言うとよく社外の方に珍しいと言われます)。

https://qiita.com/shin1kt/items/03eed49c12104002a2c7
こちらにあるような様々なショートカットを教えて頂き、いち早く見たいファイルを引っ張り出したり、直前の操作を取り消す/戻すなど、格段にスピードが速くなりました(当たり前のことかも知れませんが)。
これを通してMacのショートカットキーを覚えたりすることも多かったです。
いずれはVScodeとか使って開発したいなという思いも密かにあります。

テストの書き方

Rubyでminitestを書きました。

入社前はRSpecとともに中途半端な勉強で終わっていたので、実務を通して、調べながらであればなんとか書けるようになったかなと思います。
ただ担当PJのテスト管理が少し甘そうなので、しっかりテストを書いたら実際はどうなるのかな、と気になってはいます。

セキュリティ対策

新システム開発の中でセキュリティ対策をしました。

Railsガイド等を参考に、
・総当たり攻撃・辞書攻撃対策
・CSRF対策
・セッションハイジャック対策
・個人情報保護(クライアントやログ)
・SSL化
・エラー文表記修正(「入力されたユーザー名は登録されていません」のような具体的なメッセージにしない等)
・パスワード強化
・リダイレクトとファイル対策
あたりを一通りやったかなと思います(まだまだあると思いますが)。

今後はAWS側でのセキュリティ対策もしたいです。

Vim

いわゆるVimmerとは程遠いのですが、
本番サーバー内でdumpファイルを探したり、落ちた原因を探るためコードを直接いじってみたりして、黒い画面に抵抗がなくなる程度にはなりました。

cdで(地道に)いろんなディレクトリに寄る旅をしたりしたので、普段全く触れないようなディレクトリやファイルがまだまだたくさんあるんだなーというのも実感できました。

これで半ば強制的にLinuxの簡単な勉強にも繋がっていきました。

AWS

社内で勉強会を開催し、基礎システムの概要から掴んでみたり、PJごとにシステム構成を見ていったりしました。
ちなみに自分の案件はEC2を使わずLightsailというパッケージを使ってサーバーが構築されていたため少し特殊だったのですが、それも含め良い知見になりました。

早いうちにこちらの資格取得まで結びつけたいです。。。
AWSソリューションズアーキテクトアソシエイト

ステージング/本番環境へのデプロイ

今でもそうですが毎週のようにリリース作業を行なっているので、抵抗がないのが怖いくらいの感覚になりました(おそらく異常なんですよね、これ)。

上記で述べたようにいろんな場面で何度もやらかして先輩方に迷惑をかけまくっているので、
今では ローカルでの確認/ステージング(テスト)環境での確認・打鍵/コマンドのテンプレ化・誤字脱字チェック 等をしっかり行なう習慣が(嫌でも)つきました。

新システム作成〜システム構成〜リリース

AWSを触るということも含め、とても良い経験になりました。

DBは既存システムのものを共用したので当初「スキーマファイルはどこ??」「カラムはどうやって追加するの??」「ユーザーのパスワードが入ったかどうかってどこで確認できるの??」などパニックになったり、
まず初めにサーバーを立てようと既存サーバーのスナップショットを取ろうとしたらEC2すら使っておらず出鼻をくじかれたり、
ドメインとサブドメインの違いが分からずしばらくIPアドレスをそのまま打ち込んで画面を開いたり、
今となっては馬鹿らしいですが当時は本気で取り組んでいました。

2)顧客対応

タスク/スケジュール管理

タスク量は多くマルチタスクになりがちなので、優先順位を決めたり、話が出たら漏れのないようにすぐメモしたり、不明点はすぐ聞くようにしました。

頻繁なチャットのやりとりも含め、1つのことのなかなか集中できない難しさを痛感しています。
うまくやるコツをぜひ教えて頂きたいです。

工数見積もり

このタスクならだいたいこれくらいかかるだろうな、という予想を自分なりにするようにしていました。
それでもまだまだ多く工数を見積もってしまっているらしく、上司と答えあわせをするといつも大体半分くらいに修正されます泣

とても難しいです。

見積書/請求書の書き方、渡し方

受託はお客様に納品して初めてお金を頂くので、書類もしっかりしたものを書く必要がありました。
渡す時期やお会いした時の渡すタイミング等にも気をつけたり、PDFでもデータ送信も行なったりしました(どこまでやるかはお客様にもよると思います)。

肝心の月単位での工数計算に関しては、、、、まだまだなので、これから勉強です。

お客様を運用に乗せる気遣い/気配り

担当していた案件のお客様が、非IT/多くの部署や担当者がいる/レガシーな問題を抱えるお客様、ということも影響し、そもそも導入を嫌がっていたり、依頼したことを予定通りやってくれなかったりしました。
そのため、システムを入れるメリットや現状との具体的な比較、システムの使い方を実際に見せるなどしてお客様の抵抗を少しでも軽減できるように努めたり、食事をご一緒して少しでも親近感を持って頂けるようにしました。

協力的なお客様であれば問題なかったと思うのですが、これはお会いするたびになかなか骨の折れる業務でした。

大人数に向けたシステム説明

結構大きなリリースを控えると、60名ほどのお客様の前で自分がシステムの説明をしました。

人前で説明するのはあまり緊張しない方なので助かりましたが、丁寧に説明できなかったりうまく伝えられなかった点があり、誰でも理解できる目線での話し方と内容にしないといけないと反省しました。

ただこれが、逆にベテランエンジニアだらけだった場合にどうなってたかと考えると、、、それも恐ろしいものです。

3)その他

基本的なPC操作

そもそも自分はPCに疎かったので、現役エンジニアに囲まれながら操作することでいろいろと技を盗めました。
ファイル形式の違いやデータ管理、ショートカットあたりでしょうか。

タイピングスピードに関してはまだなかなかにひどいので、今後ブラインドタッチができるように頑張ります。

スプレッドシートの使い方

お客様と共有で使うものを中心に、スプレッドシートの使い方を覚えました。

ワードこそ前職で触っていましたが、エクセルは触ったことがなく今新たに使い方を覚えるのもちょっと億劫だったので、このタイミングでスプレッドシートだけでも使えるようになれて良かったです。

基本何にでも使えるので、やはり便利です。

チーム開発の流れ

Gitを中心に、ブランチをどのように管理するか、チーム全体でのコード管理はどうするか、プルリクをレビュー頂いたらどう修正して再レビュー頂くか、OKだったらどうマージするか、本番にはどのタイミングで反映させるか、ローカルの最新化はどうやるか、など一通り掴めました。

独学では単純に「コミット→プッシュ! コミット→プッシュ!」しかやってなかったので、現場で働かないとこれはなかなか掴めないですね。

分からない点の質問の仕方

初めは詰まったらすぐ質問してしまってましたが、まずは自分でひたすらググりまくって、それでもダメなら一体何がわからないのか質問内容をしっかり文章にまとめてから質問するようにしました。

質問内容をまとめているうちに、問題を俯瞰して見ることができたりググり方を変えたりすることに繋がったり、ググり方のコツを覚えるとだんだんとググり力も上がっていったりしたので、結構効果はあったと思います。
また周辺知識の定着度合いに比例して、「調べればわかりそう」と思うことも増えていきました。

半年〜1年でやりたいこと

ここからはほんのメモ書き程度に、自分が今後の半年で学んでいきたいスキルをバーっと書いていきます。
先にこれ勉強すればいいのに、とかありましたら、ご指摘頂けると嬉しいです。

フロント書きたい

ベタにvue.jsかreactあたり触れたいです。
、、、、の前にJSの基礎からしっかりやりたいですね。

インフラを深く知る

AWSは基礎システムしかまだ触っていないので、よく聞くけどいまいち分かってないものから知見を深めていきたいです。
とりあえずセキュリティあたりが見れるようになると嬉しい。

PLでチーム管理

責任を持って開発したり、スケジュール組んだりして、案件全体の流れを把握する経験がしたいです。
振られたタスクを必死でやってく、だけでは見えないところも多々あると思うので。

デザイン

新システムをリリースした時に、まざまざとデザイン力のなさを感じました。
Web制作みたいなところの勉強をしたいです。
有名どころのデザインの本から数冊探して勉強してみます。

IT基礎

いわゆるコンピューターリテラシーというものを全く知らないのはエンジニアとしてマズいと思うので、基礎の基礎であるITパスポートから基本情報の午前の出題範囲くらいは頭に入れておきたいです。

ググり力

マイナーな開発を調べている時に見る、英語の記事やドキュメントにまだまだ抵抗があります。
Qiita様や先人が書かれたブログ記事には日々お世話になっていますが、そろそろ幅広く読めるようになりたいです。

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

rails 部分テンプレート健忘禄

rails ~render 健忘禄~

共通部分のレイアウトをまとめて一つのファイルにしてそれを使い回す際に用いられる

code
index.html.erb
<render "パス" >

↑で記述したところに"_ファイル名.html.erb"のレイアウトが表示される。

  • パスの部分は部分テンプレートのファイル名がインスタンス変数の単数形
  • 部分テンプレートのファイル内で使う変数名が同一であること この条件を満たしていれば

code
index.html.erb
<render "@インスタンス変数名" >

と記述することができる。

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

rails gするとDid you mean? deprecate_constantとエラーがでる

コントローラとビューを作成するためrails gすると下記のエラーが起きました。
備忘録としてまとめます。

エラー文抜粋

r.rb:28:in `<class:ScaffoldControllerGenerator>'
    10: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.0.7.2/lib/rails/generators/base.rb:168:in `hook_for'
     9: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.0.7.2/lib/rails/generators/base.rb:168:in `each'
     8: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.0.7.2/lib/rails/generators/base.rb:178:in `block in hook_for'
     7: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.0.7.2/lib/rails/generators/base.rb:202:in `class_option'
     6: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/base.rb:304:in `class_option'
     5: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/base.rb:582:in `build_option'
     4: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/base.rb:582:in `new'
     3: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/parser/option.rb:11:in `initialize'
     2: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/parser/argument.rb:24:in `initialize'
1: from /Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/parser/option.rb:115:in `validate!'
/Users/****/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/thor-1.0.0/lib/thor/parser/option.rb:140:in `validate_default_type!': undefined method `deprecation_warning' for Thor:Class (NoMethodError)
Did you mean?  deprecate_constant

参考記事もなくて全く意味がわかりませんでしたが、
聞いたところGemfile.lockの下記の記載が原因らしいです。

Gemfile.lock
thor (1.0.0)

thor (1.0.0)はrails newをした時にデフォルトで作られるため、thorのバージョンを変更します。

Gemfileの最終行に下記を記載します。

Gemfile.
gem 'thor', '0.20.3'

bundle installするために一度Gemfile.lockを削除します。

削除できたらbundle installを実行します。

bundle install後、Gemfile.lockのthorが「thor (0.20.3)」となっていることを確認します。

Gemfile.lock
thor (0.20.3)

確認できたらrails gができるようになっているはずです。

細かい原因が分かってなくて恐縮ですが、以上となります。

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

【Selenium】ここらへんラッパーしとくとスッキリするんじゃないか、ってクラス 【Ruby】

クラス宣言

require 'selenium-webdriver'
require "rspec/expectations"

class BaseTest
    TEST_STR = 'test'
    TEST_NUM = '100'
    TEST_YEAR = '2019'
    TEST_MONTH = '10'
    TEST_DAY = '10'

    include RSpec::Matchers

デフォルトでテキスト入力したいときの定数を定義しておきます。
私はRSpecのアサーションだけ使いたかったのでここでインクルードしてます。

ウェブドライバー作成

def self.create_driver
    options = Selenium::WebDriver::Chrome::Options.new
    options.add_argument('--headless') #ヘッドレスモードで起動
    options.add_argument('--window-size=3000,3000') #画面サイズを設定
    driver = Selenium::WebDriver.for :chrome, options: options
end

driver = BaseTest::create_driver;

ウェブドライバーをオプション付きで起動。
毎回オプション記述するのはめんどくさいので、ここにまとめておきます。
--headlessオプションは画面を表示しないで、動作します。たぶんこれ毎回オプションで指定するよね
--window-sizeは自分の好きな画面サイズを入れといてください。
これを指定しないと、動作中にエラーが発生してスクショを取りたい時に、画面がスクロールされてると肝心の場面が写ってなかったりします。

フォームにテキスト入力

def input_str(element, value = TEST_STR)
    element.clear
    element.send_keys value
end

def input_num(element, value = TEST_NUM)
    element.clear
    element.send_keys TEST_NUM
end


@name = driver.find_element(:id, 'name')
@age = driver.find_element(:id, 'age')

input_str(@name)
input_num(@age, 20)

取得したフォーム要素を引数に指定して、なにか文字を入力します。
入力するテキストにこだわりがないのならデフォルトの文字を入力します。

数字系のバリデーションのために文字列入力と数字入力があります。

日付フォームに入力

def input_date(element, limit = true, y = TEST_YEAR, m = TEST_MONTH, d = TEST_DAY)
    if limit
        element.clear
        element.send_keys y
        element.send_keys m
        element.send_keys d
    else
        element.clear
        element.send_keys y
        element.send_keys(:tab)
        element.send_keys m
        element.send_keys d
    end
end

@date = driver.find_element(:id, data)
input_date(@date, false)

華麗に日付入力フォームに入力します。こういうの↓
スクリーンショット 2019-12-18 16.39.01.png

limitは、max属性がない日付フォームに対して入力する時に、falseを渡します。

max属性がない日付フォームは、年が6桁まで入力できてしまうため、年を入力した後にtabキーを押すことで回避しています。

スクリーンショット 2019-12-18 17.01.54.png
max属性がないとこんな感じになる。

ドロップダウンメニューを選択

def select(element, value)
    select = Selenium::WebDriver::Support::Select.new(element)
    select.select_by(:value, value)
end

@fruit = driver.find_element(:id, fruit)
select(@fruit, 1)

ドロップダウンを選択するのは面倒なのでラップしておくと捗ります。

ラジオボタン、チェックボックスを選択

def input_radio(element)
    unless element.selected?
        element.send_keys(:space)
    end
end

def input_checkbox(element)
    unless element.selected?
        element.send_keys(:space)
    end
end

@sex = driver.find_element(:id, 'sex-man')
@policy = driver.find_element(:id, 'policy')

input_radio(sex)
input_checkbox(policy)

ラジオボタンとかチェックボックスを選択された状態にします。
すでに選択された状態で押すと非選択状態になってしまうので、selected?で判定します。

ラジオボタンやチェックボックスを押すときは.clickしがちですが、.clickは要素が画面外にあると、動作してけないので、sene_keys(:space)で選択します。

配列のチェックボックスを受け取る

def input_checkboxs(elements, *values)
    elements.each do |checkbox|
        if values.include?(checkbox.attribute('value'))
            unless checkbox.selected?
                checkbox.send_keys(:space)
            end
        end
    end
end

@foods = diver.find_elements(:name, foods[])
input_checkboxs(@foods, 1, 3)

関連する複数のチェックボックスをまとめて選択します。
find_elementじゃなくてfind_elementsで取得した要素を引数で渡すことに注意。

要素はページごとにクラス変数でまとめて定義する

ラッパーではないですが、ページ内のすべての要素をクラス変数で最初に定義しておくと、変更が容易になります。

class UserEditTest < BaseInvestmentRegistTest
    @name = driver.find_element(:name, 'name')
    @id = driver.find_element(:id, 'id')
    @class = driver.find_element(:class, 'class')
    # xpathが変わったらここを変更するだけでok
    @xpath = driver.find_element(:xpass, '//*[@id="rso"]/div[2]/div/div[3]/div/div/div[1]/a/h3')

    def test_investment_regist
        #フォームに入力したり検証したり

テスト失敗時にスクショを撮る

テストをbegin~rescueで囲んで例外が発生したらスクショを撮るようにしましょう。

def test
    begin
    #フォームに入力したり検証したり
    rescue Exception => e
        puts e
        #テストに失敗したらスクショを撮る
        @driver.save_screenshot(SCREENSHOT_FILENAME)
    end
end

クラス全体

require 'selenium-webdriver'
require "rspec/expectations"

class BaseTest
    TEST_STR = 'test'
    TEST_NUM = '100'
    TEST_YEAR = '2019'
    TEST_MONTH = '10'
    TEST_DAY = '10'

    include RSpec::Matchers

    def self.create_driver
        options = Selenium::WebDriver::Chrome::Options.new
        options.add_argument('--headless')
        options.add_argument('--window-size=3000,3000')
        driver = Selenium::WebDriver.for :chrome, options: options
    end

    def input_text_str(element, value = TEST_STR)
        element.send_keys value
    end

    def input_text_num(element, value = TEST_NUM)
        element.send_keys value
    end

    def input_date(element, limit = true, y = TEST_YEAR, m = TEST_MONTH, d = TEST_DAY)
        if limit
            element.clear
            element.send_keys y
            element.send_keys m
            element.send_keys d
        else
            element.clear
            element.send_keys y
            element.send_keys(:tab)
            element.send_keys m
            element.send_keys d
        end
    end

    def select(element, value)
        select = Selenium::WebDriver::Support::Select.new(element)
        select.select_by(:value, value)
    end

    def input_checkboxs(elements, *values)
        elements.each do |checkbox|
            if values.include?(checkbox.attribute('value'))
                unless checkbox.selected?
                    checkbox.send_keys(:space)
                end
            end
        end
    end

    def input_radio(element)
        unless element.selected?
            element.send_keys(:space)
        end
    end

    def input_checkbox(element)
        unless element.selected?
            element.send_keys(:space)
        end
    end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでセルフパスワード変更ページを作ってみた

はじめに

本記事は、社内の古くなっていたシステムをリニューアルした際の手順やノウハウをまとめたものになります。
また、当記事は以前の投稿記事「Ruby on Rails6.0の環境構築から新規プロジェクト作成まで(メモ)」で作成した環境を前提に作成していますので、参考にされる方はご注意ください。

前提条件

  • 開発環境はCloud9
  • Linuxコマンドの使い方がわかる程度の力量
  • Web開発は初学者クラスの力量(ruby on rails開発未経験/Progateのレッスンは修了済)
  • HTML/CSSは難しい事はできないけど書いて読める程度の力量
  • javascriptは難しい事はできないけど書いて読める程度の力量

参考記事

RubyのNet::SSHの使い方.リモートサーバー内でsuしたりrsyncするrubyスクリプトが作れるようになる【外部サイト】
Rails6プロジェクトの各種初期設定【外部サイト】
rails newするときによく使うオプションと、rails newした後によく行う設定
bootsnapについて調べてみた
Rails5.1ではAsset Pipeline捨てたほうがいいらしいので捨ててみた
application.html.erbのレイアウトの使い方と使わない方法【外部サイト】
yieldとcontent_forを使ってページ毎にタイトルを変更【外部サイト】
最新版で学ぶwebpack 4入門JavaScriptのモジュールバンドラ【外部サイト】
Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)【外部サイト】

アプリケーションの概要

今回作成したアプリケーションは、Linux系サーバーOS上にあるユーザーのパスワードをSSH等でログインしなくても、Webページ上で変更できるにするためのものです。
「どんな時に使うの?」という話ですが、自社運用のメールサーバーをユーザー自身で定期的にパスワードを変更させたい時に使う事を想定しています。
仕様的には下記のような感じにしました。

【ざっくり仕様】

  • 入力された4つのパラメーター(ユーザーID、現行パスワード、新パスワード、新パスワード再確認)を元に対象となるサーバーへSSH接続してパスワード変更処理
  • 正常終了された場合は、注意事項などが記載されたページを表示
  • 異常終了された場合(パスワード間違いなど)は、再度元のページを表示して状況に応じて入力時の値を再設定

プロジェクト環境準備

今回はアプリケーション解説がメインのため、準備は簡単に流していきます。
まずは、プロジェクトに必要なディレクトリを作成しておきます。

ディレクトリ作成
$ mkdir -p PasswordChange/vendor/bundle/

$ tree PasswordChange

PasswordChange
└── vendor
    └── bundle

2 directories, 0 files

次にRailsのインストールからbundleインストールまで実施します。
※手順の詳細は前回の記事を参照してください。
Ruby on Rails6.0の環境構築から新規プロジェクト作成まで(メモ)

尚、今回のアプリケーションでは各種Action(Mail関連、データベース関連等)を利用しないため、Offにしてプロジェクトを作成します。

プロジェクト作成
$ bundle exec rails new . --skip-action-mailer --skip-action-mailbox --skip-action-text --skip-active-record --skip-active-storage --skip-action-cable --skip-test --skip-bootsnap --skip-turbolinks --skip-sprockets --skip-coffee --skip-bundle
各オプション 説明
--skip-action-mailer action mailer のセットアップをスキップ
--skip-action-mailbox action mailbox のセットアップをスキップ
--skip-action-text action text のセットアップをスキップ
--skip-active-record action record のセットアップをスキップ
--skip-active-storage action storage のセットアップをスキップ
--skip-action-cable action cable のセットアップをスキップ
--skip-test Minitest のセットアップをスキップ
--skip-bootsnap bootsnap のセットアップをスキップ
--skip-turbolinks turbolinks のセットアップをスキップ
--skip-sprockets sprockets のセットアップをスキップ
--skip-coffee coffee のセットアップをスキップ
--skip-bundle bundle のセットアップをスキップ

次に「webpacker」をインストールします
※オプションにそれらしいのがあったので最初はいけるかと思ったのですが、それだけでは不十分らしいので別途インストール

webpackerインストール
$ bundle exec rails webpacker:install

次に今回のアプリケーションで使用するgemを追加します。
今回追加するgemは「SSH接続用(net-ssh)」「通信確認用(net-ping)」「共通パラメーター設定用(settingslogic)」の3つになります。
※ちなみに、何故railsのインストール時に一緒に記述しないかというとGemfile上書き時に消えてしまったためです。

Gemfile追記とインストール
$ vim Genfile

【追記】
gem 'net-ping'
gem 'net-ssh'
gem 'settingslogic'

$ bundle install

次に今回作成予定の「controller」と「view」ファイルを作成します。

controllerとview作成
$ bundle exec rails g controller users_password top_form complete

      create  app/controllers/users_password_controller.rb
       route  get 'users_password/top_form'
get 'users_password/complete'
      invoke  erb
      create    app/views/users_password
      create    app/views/users_password/top_form.html.erb
      create    app/views/users_password/complete.html.erb
      invoke  helper
      create    app/helpers/users_password_helper.rb
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users_password.scss

次に今回のアプリケーション用の「routes」設定します。

routes設定
$ vim app/confing/routes.rb

【設定変更】
  get  'complete' => 'users_password#complete'
  get  'top'      => 'users_password#top_form'
  post 'top'      => 'users_password#top'
  get  '/'        => 'users_password#top_form'

「webpacker」関連の初期フォルダを作成します。
※フォルダ名はお好みでどうぞ

フォルダ作成
$ mkdir -p app/javascript/src app/javascript/stylesheets

「jquery」を「webpacker」にインストールします。

jqueryインストール
$ yarn add jquery
environment.jsへ設定追記
$ vim config/webpack/environment.js

【設定変更前】
const { environment } = require('@rails/webpacker')
module.exports = environment

【設定変更後】
const { environment } = require('@rails/webpacker')

const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)

module.exports = environment
application.jsへ設定追記
$ vim app/javascript/packs/application.js

【追記】
require("jquery")

「webpacker」でデフォルトエントリーポイント以外で、各ビュー単位のエントリーポイントを利用できるように設定を一部修正します。
※この設定をしないとコンパイルはされますが、エントリーポイント(CSS)の呼び出し(stylesheet_pack_tag)が上手くいきませんでした。
 理由は正直よくわかっていません(;^ω^)。

webpacker.ymlの修正
$ vim config/webpacker.yml

【設定変更前】
extract_css: false

【設定変更後】
extract_css: true

機能を実装

独自クラスの実装

まず、railsと直接関係ない「SSH接続パスワード変更」「共通パラメーター読込」の実装する準備をします。

フォルダと空ファイル追加
$ mkdir -p app/lib
$ touch app/lib/settings.rb app/lib/ssh_interactive.rb

次に「SSH接続パスワード変更」の機能を追加します。

このクラスでは、デフォルト設定だと通常の「ID・パスワード」でSSH接続(※1)、オプションを付け足す事で「公開鍵認証」でSSH接続します。

パスワード変更部分は対話式で「passwd」コマンド実行となるため、環境によって出力メッセージが違う場合、変更(※2)が必要となります。

※1.実はcloud9環境で作成した関係で「公開鍵認証」はテストしましたが、「ID・パスワード」はテストできていなかったりします。

※2.「if data =~ /^(current) UNIX password/ then」等の部分が該当します。

SSH接続機能追加(ssh_interactive.rb)
require 'bundler/setup'
require 'rubygems'
require 'net/ssh'
require 'net/ping'

class SshInteractive

  def initialize

    # SSH接続先アドレス
    @host = "127.0.0.1" 

    # SSH接続用オプション設定
    @option = { :port => 22 }
  end

  def set_host(host)
    @host = host
  end

  def set_port(port)
    @option[:port] = port
  end

  def set_publickey_auth(keyfile, passphrase)
    @option[:keys] = keyfile
    @option[:passphrase] = passphrase
  end

  def password_change(user_id, pass_old, pass_new, pass_verify)

    # パスワード追加
    @option[:password] = pass_old unless @option.has_key?(:keys) && @option.has_key?(:passphrase)

    # コマンド設定
    cmd = "passwd"

    # SSH接続
    begin
        return -10 unless port_scan?                          #通信接続エラー
        Net::SSH.start(@host, user_id, @option) do |ssh|
          channel = ssh.open_channel do |ch|
            channel.request_pty do |ch, success|              # ptyチェック
              return -11 unless success
            end
            channel.exec cmd do |ch, success|                 # コマンド送信と対話入力
              return -12 unless success
              ch.on_data do |c, data|                         # メッセージ取り出し
                if data =~ /^\(current\) UNIX password/ then
                  channel.send_data "#{pass_old}\n"           #パスワードを送信する
                elsif data =~ /^.+Authentication token manipulation error/ then
                  return -2                                   # パスワード間違い
                elsif data =~ /^BAD PASSWORD: The password fails the dictionary check/ then
                  return -3                                   # 辞書攻撃チェック
                elsif data =~ /^New password/ then
                  channel.send_data "#{pass_new}\n"           #パスワードを送信する
                elsif data =~ /^Retype new/ then
                  channel.send_data "#{pass_verify}\n"        #パスワードを送信する
                elsif data =~ /^.+updated successfully/ then
                  return 0
                end
              end
            end
          end
          ssh.loop                                            # SSHループ用
        end
        return -99                                            # 予想しない終了
    rescue
        return -1                                             # SSH接続エラー(ユーザーID間違い)
    end
  end

  private

  def port_scan?
    ping_tcp = Net::Ping::TCP.new(@host, @option[:port])
    return ping_tcp.ping?
  end
end

次に「共通パラメーター読込」の機能を追加します。

このクラスでは、設定値やエラーメッセージ等を一元管理するために作成しています。
設定値は「config/application.yml」に記載します。

共通パラメーター読込機能追加(settings.rb)
class Settings < Settingslogic
  source "#{Rails.root}/config/application.yml"
  namespace Rails.env
end
共通パラメーター設定
default: &default
  company: XXXXX
  system: XXXXXサーバー
  password:
    limit: 180日
    alert: 30日
  message:
    normal: パスワード変更が完了しました
    regular_access_err: パスワード変更処理を実施してください
    password_check_err: 古いパスワードと新しいパスワードが同じです
    password_verify_err: 新しいパスワードと新しいパスワード(確認)が一致しません
    password_terms_err: 複雑性を満たすパスワードになっていません
    password_lenght_err: パスワードの文字数が基準を満たしていません
    password_matchid_err: 新しいパスワードにユーザーIDと同じ文字列が含まれています
    authenticate_err: ユーザーID又はパスワードが間違っています
    password_nomatch_err: パスワードが間違っています
    dictionary_check_err: 辞書攻撃チェックに該当します
    connection_err: サーバーへの接続に失敗しています【管理者へ問い合わせてください】
    unknown_err: 予期しないエラーが発生しました【管理者へ問い合わせてください】

production:
  <<: *default
  ssh_params:
    host: 000.000.000.000
    port: 22
    keys:  /home/【ユーザー名】/.ssh/id_rsa
    passphrase: test

development:
  <<: *default
  ssh_params:
    host: 000.000.000.000
    port: 22
    keys:  /home/【ユーザー名】/.ssh/id_rsa
    passphrase: test

test:
  <<: *default
  ssh_params:
    host: 000.000.000.000
    port: 22
    keys:  /home/【ユーザー名】/.ssh/id_rsa
    passphrase: test

Web関連の機能作成(Controller)

さて、ここから本題のセルフパスワード変更ページを作成していきます。

まずは、パスワード変更処理前に完了画面に行けないようにするための設定を追加します。

before機能追加(application_controller.rb)
class ApplicationController < ActionController::Base

    def authenticate_user
        if session[:user_id] == nil
            flash[:notice] = Settings.message.regular_access_err
            redirect_to("/top")
        end
    end
end

次にパスワード変更処理の機能追加します。

パスワード変更フォームでは、最初にパスワード入力チェックを実施します。
その後、問題なければ先程作成した「SSH接続機能」を呼び出してパスワード変更処理を実施します。
特に問題なく変更できれば完了画面へ飛ばして終了です。

入力チェックエラーの場合は、フォーム画面に戻してエラーメッセージ「error_message」を表示します。
画面が切り替わったり、注視して欲しいメッセージを表示する場合は「flash」を使用しています。

パスワード変更用(users_password_controller.rb)
require 'ssh_interactive'
require 'settings'

class UsersPasswordController < ApplicationController
  before_action :authenticate_user, { only: [:complete]}

  def complete
    session[:user_id] = nil
  end

  def top_form
  end

  def top

    # パラメーター格納
    data = {
      :user_id => params[:user_id],
      :pass_old => params[:password_old],
      :pass_new => params[:password_new],
      :pass_verify => params[:password_verify]
    }

    # 旧パスワードと新パスワード比較
    if data[:pass_old] == data[:pass_new]
      data[:msg] = Settings.message.password_check_err
      error_msg(data) and return
    end

    # 新パスワード比較
    if data[:pass_new] != data[:pass_verify]
      data[:msg] = Settings.message.password_verify_err
      error_msg(data) and return
    end

    # 複雑性を満たすパスワード確認
    # 半角英小文1文字以上、半角大文字1文字以上、半角数字1文字以上、半角記号1文字以上 ! # $ % $ * + - / = @ ?
    if data[:pass_new] !~ /(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)(?=.*?[!#$%&*+\-_\/=@\?])/
      data[:msg] = Settings.message.password_terms_err
      error_msg(data) and return
    end

    # 文字数制限確認(8桁以上20桁以下)
    if data[:pass_new].length < 8 || data[:pass_new].length > 20
      data[:msg] = Settings.message.password_lenght_err
      error_msg(data) and return
    end

    # パスワード内にユーザーIDが含まれているか
    if data[:pass_new] =~ /#{data[:user_id]}/
      data[:msg] = Settings.message.password_matchid_err
      error_msg(data) and return
    end


    # SSHパスワード変更処理
    ssh = SshInteractive.new
    ssh.set_host(Settings.ssh_params.host)
    ssh.set_port(Settings.ssh_params.port)
    ssh.set_publickey_auth(Settings.ssh_params.keys, Settings.ssh_params.passphrase)
    result = ssh.password_change(data[:user_id],data[:pass_old],data[:pass_new],data[:pass_verify])

    # 結果判定
    case result
    when 0 then
      # パスワード変更確認
      flash[:notice] = Settings.message.normal
      session[:user_id] = data[:user_id]
      redirect_to("/complete")
    when -1 then
      data[:msg] = Settings.message.authenticate_err
      error_msg(data)
    when -2 then
      data[:msg] = Settings.message.password_nomatch_err
      error_msg(data)
    when -3 then
      data[:msg] = Settings.message.dictionary_check_err
      error_msg(data)
    when -10,-11,-12 then
      flash[:alert] = Settings.message.connection_err
      render("users_password/top_form")
    else
      flash[:alert] = Settings.message.unknown_err
      render("users_password/top_form")
    end
  end

  private

  def error_msg(**data)
    # 入力状態に戻してページ再表示
    @error_message = data[:msg]
    @user_id = data[:user_id]
    @password_old = data[:pass_old]
    @password_new = data[:pass_new]
    @password_verify = data[:pass_verify]
    render("users_password/top_form")
  end
end

Web関連の見た目部分作成

次に各ビューファイルを設定します。

共通画面(application.html.erb)
<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for?(:html_title) ? yield(:html_title) : "パスワード変更システム" %></title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= javascript_pack_tag 'application' %>
    <%= stylesheet_pack_tag 'application' %>
    <%= yield(:html_head) %>
  </head>

  <body>
    <header>
       <div class="header-log">
         <h1><%= Settings.company %></h1>
       </div>
    </header>
    <% if flash[:notice] %>
      <div class="flash">
        <%= flash[:notice] %>
      </div>
    <% end %>
    <% if flash[:alert] %>
      <div class="flash">
        <%= flash[:alert] %>
      </div>
    <% end %>
    <%= yield %>
  </body>
</html>

パスワード変更フォーム(top_form.html.erb)
<% content_for :html_head do %>
    <%= stylesheet_pack_tag 'top_form' %>
<% end %>

<div class="main">
  <div class="container">
    <div class="heading">パスワード変更システム</div>
    <div class="form">
      <div class="form-body">
        <% if @error_message %>
          <div class="form-error">
            <%= @error_message %>
          </div>
        <% end %>
        <%= form_tag("/top") do %>
          <p>ユーザーID</p>
          <input id="user_id" name="user_id" value="<%= @user_id %>" required max=10>
          <p>現行パスワード</p>
          <input type="password" id="password_old" name="password_old" value="<%= @password_old %>" required min=8 maxlength=20>
          <span class="field-icon">
            <i toggle="password-field" class="fas fa-eye"></i>
          </span>
          <p>新しいパスワード</p>
          <input type="password" id="password_new" name="password_new" value="<%= @password_new %>" required min=8 maxlength=20>
          <p>新しいパスワード(確認)</p>
          <input type="password" id="password_verify" name="password_verify" value="<%= @password_verify %>" required min=8 maxlength=20>
          <input type="submit" value="変更">
        <% end %>
      </div>
    </div>
  </div>
  <div class="container">
    <div class="heading">システム説明</div>
    <div class="box">
      <div class="desc-body">
        <h2>概要</h2>
        <p>当システムでは、弊社で提供している<%= Settings.system %>のパスワード変更を実施できます。</p>
        <h2>パスワードポリシー</h2>
        <p>弊社提供の<%= Settings.system %>は下記ポリシーを適用しています。</p>
        <p>様々な脅威から情報資産を守るためにも、ポリシーを遵守いただくようお願い致します。</p>
        <ul>
          <li>
            有効期限<%= Settings.password.limit %>(期限切れ利用不可)
          </li>
          <li>
            パスワード文字数は8~20文字以内
          </li>
          <li>
            パスワード複雑性(下記を含む文字列)
            <ul>
              <li>
                半角英小文字1文字以上(a-z)
              </li>
              <li>
                半角英大文字1文字以上(A-Z)
              </li>
              <li>
                半角数字1文字以上(0-9)
              </li>
              <li>
                半角記号1文字以上<br/>利用可 ! # $ % & * + - _ / = @ ? 
              </li>
              <li>
                ユーザーIDと同じ文字列禁止<br/>ユーザーID:taro<br/>新しいパスワード:Ka@1<span class="font_red">taro</span>
              </li>
            </ul>
            <li>
              その他
              <ul>
                <li>
                  現行と類似したパスワードは使用しないでください。<br/>現行パスワード:Ka@isyai12<span class="font_red">3</span><br/>新しいパスワード:Ka@isya12<span class="font_red">4</span>
                </li>
              </ul>
            </li>
          </ul>
      </div>
    </div>
  </div>
</div>
完了画面
<% content_for :html_head do %>
  <%= stylesheet_pack_tag 'complete' %>
<% end %>

<div class="main">
  <div class="container">
    <div class="heading">パスワード変更後の注意事項</div>
    <div class="box">
      <div class="body">
        <ul>
          <li>
            <p>お手数をおかけいたしますが、クライアント(パソコン等)側で設定されています<span class="font_red">パスワードの変更</span>をお願い致します。</p>
          </li>
          <li>
            <p>新しく設定したパスワードは忘れないように管理願います。</p>
            <p>万が一、パスワードがわからなくなってしまった場合は、申し訳ありませんが<span class="font_red">パスワードの再発行手続きのためにXXXの申請</span>をお願い致します。</span></p>
          </li>
          <li>
            <p>パスワードの有効期限は<%= Settings.password.limit %>です。</p>
            <p>有効期限切れ後は、<span class="font_red">パスワード忘れと同じ扱い</span>となります。</p>
            <p>尚、有効期限<%= Settings.password.alert %>前より毎日警告メールが発信されます。</p>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>

次にCSS関連の設定を追加していきます。
まずは、cssファイルとエントリーポイント登録を実施します。

各種ファイルとエントリーポイント登録
$ touch app/javascript/stylesheets/style.scss app/javascript/stylesheets/top_form.scss app/javascript/stylesheets/complete.scss

$ touch app/javascript/packs/top_form.js app/javascript/packs/complete.js

$ vim app/javascript/packs/application.js

【設定追加】
import '../stylesheets/style.scss'

$ vim app/javascript/packs/top_form.js

【設定追加】
import '../stylesheets/top_form.scss'

$ vim app/javascript/packs/complete.js

【設定追加】
import '../stylesheets/complete.scss'

次にCSSファイルの設定を追加していきます。

全ビュー共通設定(style.scss)
html {
  font: 100%/1.5 'Avenir Next', 'Hiragino Sans', sans-serif;
  line-height: 1.7;
  letter-spacing: 1px;
}

ul,li {
  list-style-type: none;
}


a {
  text-decoration: none;
  color: #2d3133;
  font-size: 14px;
}

h1, h2, h3, h4, h5, h6, p {
  margin: 0;
}

input {
  background-color: transparent;
  outline-width: 0;
}

form input[type="submit"] {
  border: none;
  cursor: pointer;
}

/* 共通レイアウト ================================ */
body {
  color: #2d3133;
  background-color: #3ecdc6;
  margin: 0;
  min-height: 1vh;
}

.main {
  position: absolute;
  top: 64px;
  width: 100%;
  height: auto;
  min-height: 100%;
  background-color: #f5f8fa;
}

.container {
  max-width: 600px;
  margin: 60px auto;
  padding-left: 15px;
  padding-right: 15px;
  clear: both;
}

/* ヘッダー ================================ */
header {
  height: 64px;
  position: absolute;
  z-index: 1;
  width: 100%;
}

.header-logo {
  float: left;
  padding-left: 20px;
  color: white;
  font-size: 22px;
  line-height: 64px;
}

/* フラッシュ ================================ */
.flash {
  padding: 10px 0;
  color: white;
  background: rgb(251, 170, 88);
  text-align: center;
  position: absolute;
  top: 64px;
  z-index: 10;
  width: 100%;
  border-radius: 0 0 2px 2px;
  font-size: 14px;
}

.font_red {
  color: red;
}
パスワード変更フォーム用(top_form.scss)
.heading {
  font-weight: 300;
  margin: 60px 0 20px;
  font-size: 48px;
  color: #bcc8d4;
}

.form {
  max-width: 600px;
  margin: 0 auto;
  background-color: white;
  box-shadow: 0 2px 6px #c1ced7;
}

.form-body {
  padding: 30px;
}

.form-error {
  color: #ff4d75;
}

.form input {
  width: 100%;
  border: 1px solid #d8dadf;
  padding: 10px;
  color: #57575f;
  font-size: 16px;
  letter-spacing: 2px;
  border-radius: 2px;
  box-sizing: border-box;
}

.form textarea {
  width: 100%;
  min-height: 110px;
  font-size: 16px;
  letter-spacing: 2px;
}

.form input[type="submit"] {
  background-color: #3ecdc6;
  color: white;
  cursor: pointer;
  font-weight: 300;
  width: 120px;
  border-radius: 2px;
  margin-top: 8px;
  margin-bottom: 0;
  float: right;
}

.form-body:after {
  content: '';
  display: table;
  clear: both;
}


.box {
  max-width: 600px;
  margin: 0 auto;
  background-color: white;
  box-shadow: 0 2px 6px #c1ced7;
}

.desc-body {
  padding: 30px;
}

.box li {
  list-style-type: square;
  display: list-item;
}

h2 {
  position: relative;
  margin: 1.5em 0em;
  padding: 0.5em;
  background: #a6d3c8;
  color: white;
}

h2:before {
  position: absolute;
  margin-bottom: 1.0em;
  content: '';
  top: 100%;
  left: 0;
  border: none;
  border-bottom: solid 15px transparent;
  border-right: solid 20px rgb(149, 158, 155);
}
完了画面用(complete.scss)
.heading {
  font-weight: 300;
  margin: 60px 0 20px;
  font-size: 36px;
  color: #bcc8d4;
}

.box {
  max-width: 600px;
  margin: 0 auto;
  background-color: white;
  box-shadow: 0 2px 6px #c1ced7;
}

.body {
  padding: 30px;
}

.box li {
  list-style-type: square;
  display: list-item;
}

後はどうでもいいおしゃれポイントとして「flash」が表示された後、
5秒後にフェードアウトするjsを追加します。

javascriptファイル追加
$ touch app/javascript/src/flash_message.js

$ vim app/javascript/packs/application.js

【設定追加】
import '../src/flash_message.js'
メッセージフェードアウト(flash_message.js)
(function() {
    setTimeout("$('.flash').fadeOut('slow')", 5000)
})

これで一応作成完了です。
テスト機能とか本番環境用の設定とか色々ありますが、
それはまた別記事で紹介したいなと考えています。

アプリケーション作成後の所感

題材選びに、失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した

なんでオーソドックスなMVCモデルのアプリではなく、
DBを必要としない対話アプリを最初の題材に選んだのかというのが正直な感想。
※業務に関連する直近の課題をチョイスした結果なんですけどね。

おまけに、無駄なこだわりを色々入れてみた結果、
工数1日で作成完了していたアプリケーションが工数10日(ほぼ調査)まで膨れ上がってしまった。

これを見た初学者の皆様は題材選びと仕様設定には気を付けてください。

(おまけ)無駄にこだわって調べた点

  • js、cssの管理をWebpackerで統一
  • SSH接続部分は別クラスで実装したい
  • 共通パラメーターの外部ファイル化
  • css等のファイルをビュー単位で分割(applicationに書くのが嫌だった)
  • 各ビューからhead内にCSSファイルを入れる
  • プロジェクトで無駄に生成されるファイルを極力減らす
  • 大してテストケースもないのに態々System Specを実装(別記事で紹介予定)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby で DistIP,DistPort,SourceIP,SourcePort を指定して Tcp 通信を行うコード

きっかけ

通信の検証で5-tupleの影響を受けているのか確認する必要があった

5-tupleとは

IPヘッダに存在する5つの要素のことで

1.Source IP
送信元アドレス
2.Destination IP
宛先アドレス
3.Source Port
送信元ポート
4.Destination Port
宛先ポート
5.Protocol
プロトコル

を指す

tcp.rb
require 'socket'

distIp     = "111.111.111.111"
distPort   = 80
sourceIp   = "222.222.222.222"
sourcePort = 100

begin
  s = TCPSocket.open(distIp,distPort,sourceIp,sourcePort)
  s.print "GET / HTTP/1.0\r\n\r\n"
  print s.read
ensure
  s.close if s
end

メモ

サーバに送信している部分で下記のヘッダー情報が必要かもしれないと思った

tcp.rb
  s.print "GET / HTTP/1.0\r\nUser-Agent: curl/7.19.7\r\nHost: localhost\r\nAccept: */*\r\nConnection: close\r\n\r\n"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Homebrewのruby-buildの更新を待たずにrbenvで最新版のRubyをインストールする方法

はじめに

Homebrewでrbenvとruby-buildの組み合わせでRubyをインストールしている場合は、以下のコマンドで最新版のインストールパッケージを取得することができる。

brew upgrade rbenv ruby-build

ただし、最新のruby-buildがHomebrewに反映されるまでには数日かかることがあるため(※)、最新バージョンのRubyがリリースされてもすぐにインストールできないことがある。

※ 参考情報
ruby-buildの更新に時間がかかるのは、HomebrewへのPull request(以下PR)の作成とマージが必要になるから。
→ 例:ruby-build 20191205のPR

Homebrewの更新を待たずにすぐにインストールしたい場合は、以下の手順で自力で最新版のインストールパッケージをセットアップすることができる。

手順1. ruby-buildのディレクトリへ移動する

ターミナルから以下のコマンドを入力してruby-buildのディレクトリへ移動する。

なお、下記コマンドの20191205の部分はローカルにインストールされているruby-buildのバージョンに応じて変わるので、適宜書き換えること。

cd /usr/local/Cellar/ruby-build/20191205/share/ruby-build

手順2. ruby-buildのPull requestを確認する

ruby-buildのcloseされたPRを確認し、目的のバージョンがすでに取り込まれていることを確認する。

https://github.com/rbenv/ruby-build/pulls?q=is%3Apr+is%3Aclosed

ここでは下記のPR(2.7.0-rc1を対象にしたもの)を例に取り上げる。

https://github.com/rbenv/ruby-build/pull/1389

手順3. PRで追加されたファイルをローカルに作成する

PRで追加されたファイルと同じ内容のファイルをローカルに作成する。

touch 2.7.0-rc1 
vim 2.7.0-rc1 

追加したファイルの中身は以下のようになる。(2.7.0-rc1の場合)

install_package "openssl-1.1.1d" "https://www.openssl.org/source/openssl-1.1.1d.tar.gz#1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2" mac_openssl --if has_broken_mac_openssl
install_package "ruby-2.7.0-rc1" "https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.0-rc1.tar.bz2#1c5a02b63fa9fca37c41681bbbf20c55818a32315958c0a6c8f505943bfcb2d2" ldflags_dirs enable_shared standard verify_openssl

手順4. インストールを実行する

ファイルを保存したら、インストール可能なバージョンが増えていることを確認する。

$ rbenv install -l | grep 2.7.0
2.7.0-dev
2.7.0-preview1
2.7.0-preview2
2.7.0-preview3
2.7.0-rc1       <= 新しく増えた
jruby-9.2.7.0

目的のバージョンが追加されていることを確認したら、インストールを実行する。

rbenv install 2.7.0-rc1

まとめ

手順は以上です。もっといいやり方をご存じの方がいたらコメントをお願いします!

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

Windowsユーザーの私がrbenvでRubyのインストール

Windowsユーザーの私がrbenvでrubyのインストール

macと違い、windowsではPCにrbenvを直接インストールできない
準備として、windows内にubuntuという環境を作る必要がある。

ubuntu環境は、windows内に作るもので、ここからの話は、windows内というよりは、windows内のubuntu内で起きることである。
(調べ方も当然、"ubuntu rbenv インストール"となる)
(注)rbenvだけだと、どうしてもmacのことばかり出てきてしまうので気をつける。

しかし、PC自体の容量は空けとかないと、ここからのインストールの容量が足りないから、Windowsが無関係ってわけではない
(Windows内のubuntuが様々なものをインストールし、大きくなっていくから、ubuntuをかかえるWindowsの容量がないと、かかえきれない)

https://mhaya18.hatenablog.com/entry/2018/09/01/090051
↑このサイトがおススメである。

git clone https://github.com/rbenv/rbenv.git ~/.rbenv

echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc

~/.rbenv/bin/rbenv init

以上を入力して実行すると、
sudo apt install rbenv
ってやれば、rbenvをインストールできるよと教えてくれる。

そこから言われた通り、
sudo apt install rbenv
でダウンロード

途中でエラーすることがある

その際は前に述べたように、容量の確認
df -h
で確認できる。

ちなみに自分はPC本体の容量がいっぱいいっぱいだった
:$ df -h
Filesystem Size Used Avail Use% Mounted on
rootfs 235G 235G 18M 100% /
none 235G 235G 18M 100% /dev
none 235G 235G 18M 100% /run
none 235G 235G 18M 100% /run/lock
none 235G 235G 18M 100% /run/shm
none 235G 235G 18M 100% /run/user
C: 235G 235G 18M 100% /mnt/c

(このパターンは稀で、容量不足としては仮想環境に割り当てられるディスク容量が足りないことが多いんだとか)

データの削除をして、再びインストール実行

eval "$(rbenv init -)"

curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash

↑二つを実行し、確認できた。

その後、
メッセージででてきた手段でrubyインストール

rbenv install 2.5.3

rubyの構築が完了した

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

getsメソッドで入力された値を取得 ❏Ruby❏

Rubyでgetsを読み込むとターミナルで入力待ちになります。
そこで入力された値を文字列として返します。

実際にやっていきやす!

getsだけだと勝手に改行されるので、それを防ぐために.chompを付けます。

sample.rb
word = gets.chomp
puts word
ターミナル
$ ruby sample.rb
##入力待ち

Hello World!
##適当に打ち込みenterを押すと…

Hello World!
##結果が表示される

※あくまで文字列を返す

getsは数字を打ち込んでも文字列として返します。
数値として返したい場合は、.to_iを付けます。

sample.rb
number = gets.to_i
puts number + 3
ターミナル
$ ruby sample.rb
##入力待ち

5
##適当に打ち込みenterを押すと…

8
##結果が表示される
##numberには数値が入ったことがわかる。



【結論】
文字列を期待している場合は、gets.chomp
数値を期待している場合は、gets.to_i
を使う。



ではまた!

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

自分なりのテストの書き方 by RSpec

くふうカンパニーアドベントカレンダー20日目の記事です。

今年の新卒研修の中で自動テストの研修を担当してRSpecを使ったテストの書き方を説明しました。
しかしながら新卒エンジニア達からRSpecの使い方はわかったけれど、テストってどうやって書いていけばいいのと質問を受けたので私が普段どのようなステップを踏んでテストを書いているか振り返ってみることにしました。まだまだ不十分な研修で申し訳ない気持ちです?
使用するテストフレームワークは実務で利用しているRSpecです。

新卒たちの質問

  • そもそもテストって何をテストするの
  • どこまでテストを書けばいいの
  • どこから書き始めてばいいの

そもそもテストって何をテストするの

作っている機能が動作することを確認(テスト)します。
例えば名前とメールアドレスを登録できる機能があったとします。ユーザ目線ではフォームから名前とメールアドレス入力して確定すると、登録されたというメッセージと登録情報が画面に表示される機能となります。そして内部構造に注目するとデータの有効性を検証しデータベースに保存されて登録完了画面にリダイレクトする機能になります。
これらの機能が仕様通り振る舞いが正しく動作しているかテストで確認するようにします。
ユーザ目線で仕様通り動作するか確認するテストはE2EテストとしてSystem specで、開発者目線で動作しているメソッドの振る舞いを確認するテストはユニットテストとしてModel specやController specで書いています。
今回はとりあえずModel specを例に書きました。

どこまでテスト書けばいいの

どの程度の品質基準を目指すのかはソフトウェアテストの基準を参考にしています。

例としてUserクラスに以下のようなメソッドがあったとします。
ユニットテストに関してはホワイトボックステストの条件網羅(C2)を目指して書いています。下のようにシンプルな処理の時は分岐網羅(C1)で留めているものもあったりしますが。。

def able_to_send_mail?
if name && email
  true
else
  false
end

条件網羅の時は、nameとemailの両方がtrue、emailのみfalse、nameのみfalseの3つのテストケースのテストを書くようにします。nameがfalseであればemailの真偽は判定に影響しないのでnameとemailがfalseのケースは省略しています。条件を書くか省くか迷った場合は書いておけば漏れはないので問題はありません。

name email name.present? email.present? if文の判定
ケース1 'bob' 'bob @mail.com' true true true
ケース2 'bob' nil true false false
ケース3 nil 'bob @mail.com' false true false

分岐網羅の時はこのようなコードがあった場合はif文がtureになるテストケースとfalseになるテストケースの2種類のテストを書くようにします。

name email name.present? email.present? if文の判定
ケース1 'bob' 'bob @mail.com' true true true
ケース2 nil 'bob @mail.com' false true false

どこから書き始めればいいの

テストで網羅するテストケースはわかったので次は実際テストを順序立てて組み立てていきます。
テスト用のファイルが自動生成されていればそれを使って、なければ新しく作成します。

テスト対象を書く

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
  end

  # 他のメソッドがある場合は同じインデントで並べて書きます
  describe '他のメソッド' do
  end
end

describeでテスト対象のメソッド名を書きます。
他にもテスト対象のメソッドが追加された場合は同じインデントでテストを追加していきます。

あり得るテストケースを書き出す

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
    context 'nameとemailが存在する場合' do
    end
    context 'emailがnilの場合' do
    end
    context 'nameがnilの場合' do
    end
  end
end

contextで先ほど網羅したメソッドの実行する時にあり得るテストケースを書き出します。コンテキストをネストして書くこともできますが、ネストが3重くらいになってくるような場合はメソッドの設計を見直すことを考えます。
日本語で書くか英語で書くかは全体で決まっていればどちらでもいいと思います。

期待する結果を書き出す

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
    context 'nameとemailが存在する場合' do
      it 'trueを返すこと' do
      end
    end
    context 'emailがnilの場合' do
      it 'falseを返すこと' do
      do
    end
    context 'nameがnilの場合' do
      it 'falseを返すこと' do
      end
      # 他にも期待する結果がある場合は書きます
      it '他の期待値' 
      end
    end
  end
end

itを使って期待する結果を書き出します。
ここまで書けると後からテストを見た時にメソッドがどのような振る舞いをするかわかるようになってきました。

テストコードを実装する

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
    context 'nameとemailが存在する場合' do
      it 'trueを返すこと' do
        user = User.create(name: 'bob', email: 'bob@mail.com')
        expect(user.able_to_send_mail?).to eq true
      end
    end
    context 'emailがnilの場合' do
      it 'falseを返すこと' do
        user = User.create(name: 'bob', email: nil)
        expect(user.able_to_send_mail?).to eq false
      do
    end
    context 'nameがnilの場合' do
      it 'falseを返すこと' do
        user = User.create(name: nil, email: 'bob@mail.com')
        expect(user.able_to_send_mail?).to eq false
      end
    end
  end
end

期待する結果を検証するためのコードをexpectを使って書いていきます。
ここまでできれば一旦テストとしては完成です。

テストのリファクタリング

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#able_to_send_mail?' do
    subject { user.able_to_send_mail? }
    context 'nameとemailが存在する場合' do
      let(:user) { User.create(name: 'bob', email: 'bob@mail.com') }
      it { is_expected.to eq true }
      end
    end
    context 'emailがnilの場合' do
      let(:user) { User.create(name: 'bob', email: nil) }
      it { is_expected.to eq false }
    end
    context 'nameがnilの場合' do
      let(:user) { User.create(name: nil, email: 'bob@mail.com') }
      it { is_expected.to eq false }
    end
  end
end

重複を省いたり変数をletに書き出したりリファクタリングしました。
expect(user.able_to_send_mail?)はsubjectでまとめて、ローカル変数だったuserはletを使うようにしました。期待する値も真偽値のみなので自然言語で書かれた期待値は削除しました。
もっとも過度にリファクタリングしてコードの重複を省いた結果、コードを追うことが難しくなるようであれば後からメソッドの振る舞いを知るのが難しくなってしまうので、ある程度の重複は許容してもいいと考えています。

まとめ

今回は私がどのように考えながらテストコードを書いていっているか振り返ってみました。
今年のフィードバックを糧に今後の新卒たちの研修を改善していきたいと思います。

合わせて読みたい

第6回 Webアプリケーションのテスト
ホワイトボックステストにおけるカバレッジ(C0/C1/C2/MCC)について
【初心者向け】テストコードの方針を考える(何をテストすべきか?どんなテストを書くべきか?)
RSpecえかきうた

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

Rails6 のちょい足しな新機能を試す112(database.yml warning編)

はじめに

Rails 6 に追加された新機能を試す第112段。 今回は、database.yml warning 編です。
Rails 6 では、複数データベースに対応していますが、 database.yml ファイルで複雑なERBが使われていると警告が出るようになっています。

Ruby 2.6.5, Rails 6.0.1 で確認しました。 (Rails 6.0.0 でこの修正が入ったようです。)

$ rails --version
Rails 6.0.1

今回は、警告を出すために、 環境変数 DB によって、 MySQLデータベースかPostgreSQLデータベースにアクセスを切り変えるということで試してみたいと思います。
複数データベースの機能は使いません。

Rails プロジェクトを作成する

$ rails new rails_sandbox
$ cd rails_sandbox

Gemfile に mysql2 と pg を追加する。

MySQL と PostgreSQL どちらでも接続できるように、 Gemfile に mysql2 と pg を追加します。

Gemfile
gem 'pg', '>= 0.18', '< 2.0'
gem 'mysql2'

bundle install を実行します。

$ bundle install

database.yml ファイルを編集する

database.yml を変更します。

MySQL 用の接続設定と PostgreSQL 用の接続設定を追加します。

環境変数によって、接続を切り変えられるようにします。

ここで、ポイントは、 <<: *<%= ENV.fetch("DB") { 'postgresql' } %> となっている部分です。
この記述によって警告が出るようになります。

config/database.yml
mysql: &mysql
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: db_mysql

postgresql: &postgresql
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: db_postgresql
  user: <%= ENV.fetch("POSTGRES_USER") %>
  password: <%= ENV.fetch("POSTGRES_PASSWORD") %>

development:
  <<: *<%= ENV.fetch("DB") { 'postgresql' } %>
  database: app_development
  ...

test:
  <<: *<%= ENV.fetch("DB") { 'postgresql' } %>
  database: app_test
  ...

production:
  <<: *<%= ENV.fetch("DB") { 'postgresql' } %>
  database: app_production
  ...

スクリプトを作成する

確認のためだけなのですが、 database.yml を ERB でパースして結果を出力するスクリプトを書いておきます。

scripts/parse_database_yml.rb
yml = ERB.new(File.read(Rails.root.join('config/database.yml')))
print yml.result

スクリプトを実行する

スクリプトを実行します。特にエラーもなくパースできています。

development: の次の行が、 <<: *postgresql と処理できていることに注意してください。

$ bin/rails runner scripts/parse_database_yml.rb
Running via Spring preloader in process 104
mysql: &mysql
  adapter: mysql2
  ...

postgresql: &postgresql
  adapter: postgresql
  ...

development:
  <<: *postgresql
  database: app_development
  ...

db:create を実行する

db:create を実行します。
Rails couldn't infer whether you are using multiple databases from your database.yml ... please simplify your ERB. と警告が出ます

$ bin/rails db:create
Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB.
Created database 'app_development'
Created database 'app_test'

なぜ警告が出るのか

複数データベースに対応するために、 Rails は、 boot 完了する前に、 database.yml を読んで、task を生成しているそうです。task 生成する時点では、 ERB を使っておらず DummyCompiler を使って解析しており、この DummyCompiler では処理できない場合に警告を出すようにしたということらしいです。

どうすれば良いのか

結論としては、ERB の部分を警告が出ないように書き変えれば良いということになります。
では、一般的にどう書き変えれば良いのかは、筆者にもわかっていません。

ワーニングを出ないように修正する

今回の場合は、以下のように書き変えることで警告が出ないようにすることができました。
default を追加して if 文使って切り変えるようにして、 development などの環境では、 << *default を使って ERB を使わないように修正しました。

config/database.yml
mysql: &mysql
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: db_mysql

postgresql: &postgresql
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: db_postgresql
  user: <%= ENV.fetch("POSTGRES_USER") %>
  password: <%= ENV.fetch("POSTGRES_PASSWORD") %>

default: &default
  <% database = ENV.fetch("DB"){ 'postgresql' } %>
  <% if database == 'postgresql' %>
  <<: *postgresql
  <% else %>
  <<: *mysql
  <% end %>

development:
  <<: *default
  database: app_development
  ...

test:
  <<: *default
  database: app_test
  ...

production:
  <<: *default
  database: app_production
  ...

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try112_warn_database_yml - 警告が出る方
https://github.com/suketa/rails_sandbox/tree/try112_warn_database_yml_fix - 警告が出ないように修正した方

参考情報

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

.present?はnilかそうじゃないか、という場合は不要

キャンペーンコードから適用されるキャンペーンとプランに紐づくキャンペーンがあるんだけど、キャンペーンコードからのキャンペーンを配列の一番最初に表示させたかった時の実装。

辞書型(hash型)なのが注意。
keyでキャンペーンコードからのものを探して一旦削除してunshiftで先頭にpushしてるんだけどいまいち納得いってない。。もっといい方法ないのかな?

修正前

order_base_decorator.rb
  def order_campaigns
    c = options['campaigns'].to_a
    cc = c.find{|x| x["key"] == "campaign-code"}
    c.delete(cc) if cc.present?
    c.unshift(cc)  if cc.present?
    if c.present?
      unless c.first == {} || cc
        return c.unshift({}).to_a
      end
      return c
    end
    return c
  end

修正後

order_base_decorator.rb
  def order_campaigns
    c = options['campaigns'].to_a
    cc = c.find{|x| x["key"] == "campaign-code"}
    # cのなかで見つからなかったらccはnil 
    if cc
      c.delete(cc)
      c.unshift(cc)
    end
    if c.present?
      unless c.first == {} || cc
        return c.unshift({}).to_a
      end
      return c
    end
    return c
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】NaNのNaNa不思議

これは Ruby Advent Calendar 2019の18日目の記事です。Otemachi.rb #17 - connpassで話した内容の大幅加筆修正版です。

実用性は皆無ですので、求めている方はブラウザバックしてください。

NaN (Not a Number) とは

みなさん NaN ってご存知でしょうか?

NaNとは Not a Number の略で、日本語では非数といいます。主に浮動小数点演算の結果が不定形だったり未定義だったりするときに返ってきます。

constant Float::NAN (Ruby 2.6.0 リファレンスマニュアル)

その数がNaNであるかどうかは、Float#nan?で調べることができます。

NaNの仕様はIEEE 754で規定されているので、大抵のプログラミング言語には実装されています。

JavaScriptだとたまに見かけますが、RubyだとNaNを返すのではなく例外を発生させるケースが多い1ため、ほぼ見かけないですね。

NaNが返ってくるパターン

他にもあったらコメント欄などで教えて下さい。

Float::NANを代入する

irb(main):001:0> nan = Float::NAN
# => NaN

そりゃそうだ

NaNと四則演算をする

Float::NAN + 1
# => NaN
Float::NAN - 1
# => NaN
Float::NAN * 1
# => NaN
Float::NAN / 1
# => NaN

上記のような四則演算に限らず、NaNと計算した結果はNaNになります。

つまり、NaNは感染します

浮動小数点で不定形を計算する

「不定形」という言葉の意味、高校で理系数学をやった方はわかると思いますが、これを計算しようとするとNaNになります。

0/0.0

0/0.0
# => NaN

こうなるのは浮動小数点数だけで、整数で 0/0とするとZeroDivisionErrorが返ってきます。

この違いは、両者がもつ意味の違いではないかと思います。

整数の 0は正確に0である一方で、浮動小数点数でx = 0.0とすると0 <= x < 0.05を満たす数を表す、という意味をもつ、という違いがあります。

0/0.0はゼロ除算ではないのかもしれないけど、値を計算できないから非数となるのですね。

INFINITY/INFINITY

Float::INFINITY / Float::INFINITY
# => NaN

Floatクラスの中では「無限大」を表す Float::INFINITYが定義されています。

ただ無限大を無限大で割ってもどの値に収束するかわかりませんね。これもNaNです。

INFINITY - INFINITY

Float::INFINITY - Float::INFINITY
# => NaN

こちらもNaNになります。

NaNのNaNa不思議

ここからは、NaNのNaNa不思議、もとい変わった性質や挙動を見ていきましょう。

1. 自分どうしを==で比較するとfalseが返ってくる

Float::NAN == Float::NAN
# => false

Rubyでこの挙動をする唯一の値ではないでしょうか。

これを利用した問題が、RubyKaigi 2019のエムスリーさんのブースで出題されてました。

難読Rubyコードクイズ問題と解説 in RubyKaigi 2019 - エムスリーテックブログ(Day3-3)

2. truthyである

Float::NAN ? 'hoge' : 'piyo'
# => "hoge"

!!Float::NAN
# => true

nilfalse以外はすべてtruthy」というRubyのルールにしたがって、NaNもtrueと判定されます。

これはそこまで変わった性質じゃないですが、決して比較できない値がきっちりtrueと判定されるというのはなんだか不思議です。

3. 複素数に変換できる

Float::NAN.to_c
# => (NaN+0i)

お役所仕事という感じがします。

4. 複素数どうしで0.0/0.0をすると実部も虚部もNaNになる

Complex(0.0, 0.0) / Complex(0.0, 0.0)
# => (NaN+NaN*i)

複素数つながりでもう一個。

複素数の割り算は分母の共役複素数を分母分子にかけて分母を実数にする、と教わりましたが、分母のゼロになにをかけてもやっぱりゼロです。

5. 数値なのにsingletonではない

前提として、 Rubyの数値はsingletonです2

n = 1
m = 2
n.object_id == (m-1).object_id
# => true

計算しても、同じ値なら同じオブジェクトになります。

0xff.object_id == 255.object_id
# => true

(1.2e-0).object_id == (0.12*10).object_id
# => true

このように、表記が違ったり整数でなかったりしても同じ数値は同じオブジェクトです。処理系全体で同じ数値のオブジェクトは1つしか存在しないんですね。

でも、NaNの場合は違います。NaNに1足してもNaNですが、

nan.object_id == (nan+1).object_id
# => false

やっぱり一味違いますね。

6. ハッシュのキーになることができるが、値が取り出せなくなることがある

h = {}

h[:a] = 1
h[:a]
# => 1

h[0/0.0] = 2
h[0/0.0]
# => nil

こうなると、普通の方法ではこの「2」は取り出せません。

取り出す方法は色々あると思いますが、一例。

h.to_a
# => [[:a, 1], [NaN, 2]]

h.to_a[1][1]
# => 2

ただし、Float::NANをキーに使った場合や変数にNaNを入れた場合はちゃんと取り出すことができます。

h = {}
h[Float::NAN] = 3
h[Float::NAN]
# => 3

n = 0/0.0
h[n] = 4
h[n]
# => 4

ハッシュのキーを比較するときには Float#eql?メソッドが使われているはず(参考: るりまのObject#eql?)なのですが、前述の通り NaNとの比較は無条件でfalseを返す ことになっている3ので、謎です。

見た感じオブジェクトIDを比較している、つまりBasicObject#equal?で比較しているのですが、それらしいソースコードは見つけられず……

なお、出典はこちらの記事です。

HashのキーをNaNにすると何が起きるか - Qiita

7. 配列に入れると==trueを返す

Float::NAN == Float::NAN
# => false

[Float::NAN] == [Float::NAN]
# => true

配列以外のコンテナでも同様の挙動をします。

{ Float::NAN => 0 } == { Float::NAN => 0 }
# => true

こちらも上記同様にBasicObject#equal?で比較していると思われる怪奇現象です。情報求む。

なお、出典はこちらの記事です。

`Float::NAN` についての重箱の隅 - Qiita

まとめ

NaNの挙動はよくわかりません。

それではみなさん、良いお年を。

バージョン情報

$ ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]

参考文献


  1. 整数どうしのゼロ除算0/0や、負数の平方根Math.sqrt(-1)など 

  2. ruby/numeric.c at master · ruby/rubyを読んでみましたが、該当部分を見つけられませんでした。。。 ので、正確な出典はありません。ご存じの方、教えて下さい 

  3. ruby/numeric.c at master · ruby/ruby 

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

Rails6でRSpecのコントローラテストをしようとするとエラーが発生する

はじめに

Everyday Rails - RSpecによるRailsテスト入門を見ながら、
勉強がてらアプリにRSpecを追加していましたが、
Rails6特有のエラーが発生しましたのでご紹介します。

2019/12/18追記

Everyday Rails - RSpecによるRailsテスト入門 の著者の一人である、
@jnchito さんがRails6でRSpecを使用する際の変更点を挙げられた記事があります。

Rails6でRSpecを使用する際の変更点が網羅されていますので、
ぜひご参照ください。

【動画付き】Everyday RailsのサンプルアプリをRails 6で動かす際に必要なテストコードの変更点 - give IT a try

開発環境のバージョン

$ ruby -v
ruby 2.6.4p104 (2019-08-28 revision 67798) [x86_64-darwin18]
$ rails -v
Rails 6.0.2

rspec-rails 3.9.0 を使用していました。

発生したエラー

下記のようなコントローラのスペックファイルを書いて実行すると、
警告とエラーが発生しました。

spec/controllers/static_pages_controller_spec.rb
  describe '#home' do
    it "正常なレスポンスを返す" do
      get :home
      expect(response).to be_success
    end
  end
$ rspec spec/controllers/static_pages_controller_spec.rb 

StaticPagesController
  #home
DEPRECATION WARNING: formats is deprecated and will be removed from Rails 6.1 (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6)
DEPRECATION WARNING: ActionView::Template#initialize requires a locals parameter (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6)
DEPRECATION WARNING: formats is deprecated and will be removed from Rails 6.1 (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6)
DEPRECATION WARNING: ActionView::Template#initialize requires a locals parameter (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6)
DEPRECATION WARNING: formats is deprecated and will be removed from Rails 6.1 (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6)
DEPRECATION WARNING: ActionView::Template#initialize requires a locals parameter (called from block (3 levels) in <top (required)> at /Users/amatsuki/programing/portfolio/study_roadmap/spec/controllers/static_pages_controller_spec.rb:6)
    正常なレスポンスを返す (FAILED - 1)

Failures:

  1) StaticPagesController#home 正常なレスポンスを返す
     Failure/Error: get :home

     ActionView::Template::Error:
       wrong number of arguments (given 2, expected 1)
     # ./spec/controllers/static_pages_controller_spec.rb:6:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # ArgumentError:
     #   wrong number of arguments (given 2, expected 1)
     #   ./spec/controllers/static_pages_controller_spec.rb:6:in `block (3 levels) in <top (required)>'

原因

Rails6でのRSpecのバグのようです。
Issuesにもいくつか挙げられていました。

github/rails - Warning formats is deprecated and will be removed from Rails 6.1

github/rspec-rails - Warning formats is deprecated and will be removed from Rails 6.1

github/rspec-rails - Changes In Rails 6 ActionView::Template Breaks EmptyTemplateHandler

rails-rspecのバージョンを4.0.0.beta3にする

先ほどのリンクで指摘されていたように、
rails-rspecのバージョンを4.0.0.beta2にあげます。

まずはコマンドでインストールできるバージョンの中に4.0.0.beta2があるか、
他に最新のバージョンはあるか確認します。
下記のgem queryはローカルまたはリモートリポジトリのgemの情報を取得するものです。

$ gem query -rae --prerelease 'rspec-rails'

今回使用したオプションの意味は下記のようになっています。

オプション 意味
-r リモートに指定
-a 全てのgemのバージョンを表示
-e 完全一致したgemを指定
--prerelease プレリリースバージョンも表示

gemコマンドの詳細はrubygems.orgのページを参照してください。
rubygems - Guides

gem queryコマンド実行

コマンドを実行してみると、
4.0.0.beta2があることを確認できました。

さらに、4.0.0.beta3があることも確認できました。(2019/12/18現在)
バージョンは高い方がよしとされていますので、今回は4.0.0.beta3の方をインストールします。

$ gem query -rae --prerelease 'rspec-rails'

*** REMOTE GEMS ***

rspec-rails (4.0.0.beta3, 4.0.0.beta2, 3.9.0, 3.8.3, 3.8.2, 3.8.1, 3.8.0, 3.7.2, 3.7.1, 3.7.0, 3.6.1, 3.6.0, 3.6.0.beta2, 3.6.0.beta1, 3.5.2, 3.5.1, 3.5.0, 3.5.0.beta4, 3.5.0.beta3, 3.5.0.beta2, 3.5.0.beta1, 3.4.2, 3.4.1, 3.4.0, 3.3.3, 3.3.2, 3.3.1, 3.3.0, 3.2.3, 3.2.2, 3.2.1, 3.2.0, 3.1.0, 3.0.2, 3.0.1, 3.0.0, 3.0.0.rc1, 3.0.0.beta2, 3.0.0.beta1, 2.99.0, 2.99.0.rc1, 2.99.0.beta2, 2.99.0.beta1, 2.14.2, 2.14.1, 2.14.0, 2.14.0.rc1, 2.13.2, 2.13.1, 2.13.0, 2.12.2, 2.12.1, 2.12.0, 2.11.4, 2.11.0, 2.10.1, 2.10.0, 2.9.0, 2.9.0.rc2, 2.8.1, 2.8.0, 2.8.0.rc2, 2.8.0.rc1, 2.7.0, 2.7.0.rc1, 2.6.1, 2.6.1.beta1, 2.6.0, 2.6.0.rc6, 2.6.0.rc4, 2.6.0.rc2, 2.5.0, 2.4.1, 2.4.0, 2.3.1, 2.3.0, 2.2.1, 2.2.0, 2.1.0, 2.0.1, 2.0.0, 2.0.0.rc, 2.0.0.beta.22, 2.0.0.beta.20, 2.0.0.beta.19, 2.0.0.beta.18, 2.0.0.beta.17, 2.0.0.beta.16, 2.0.0.beta.15, 2.0.0.beta.14.2, 2.0.0.beta.14.1, 2.0.0.beta.13, 2.0.0.beta.12, 2.0.0.beta.11, 2.0.0.beta.10, 2.0.0.beta.9.1, 2.0.0.beta.8, 2.0.0.beta.7, 2.0.0.beta.6, 2.0.0.beta.5, 2.0.0.beta.4, 2.0.0.beta.3, 2.0.0.beta.2, 2.0.0.beta.1, 2.0.0.a10, 2.0.0.a9, 2.0.0.a8, 2.0.0.a7, 2.0.0.a6, 2.0.0.a5, 2.0.0.a4, 2.0.0.a3, 2.0.0.a2, 1.3.4, 1.3.3, 1.3.3.rc, 1.3.2, 1.3.1, 1.3.0, 1.2.9, 1.2.7.1, 1.2.7, 1.2.6, 1.2.5, 1.2.4, 1.2.3, 1.2.2, 1.2.1, 1.2.0, 1.1.12, 1.1.11, 1.1.10, 1.1.9, 1.1.8, 1.1.7, 1.1.6, 1.1.5)

bundleでインストール

bundleを使用しているのでGemfileを下記のように修正します。

Gemfile
  gem 'rspec-rails', '4.0.0.beta3'

そして、インストールします。

$ bundle

再度テストを実行

めでたしめでたし、、、と思いきやまたもやエラーが発生。。。

$ rspec spec/controllers/static_pages_controller_spec.rb 

StaticPagesController
  #home
    正常なレスポンスを返す (FAILED - 1)

Failures:

  1) StaticPagesController#home 正常なレスポンスを返す
     Failure/Error: expect(response).to be_success
       expected #<ActionDispatch::TestResponse:0x00007ff5c5326f78 @mon_mutex=#<Thread::Mutex:0x00007ff5c5326be0>, @mo...ders:0x00007ff5c536d4f0 @req=#<ActionController::TestRequest:0x00007ff5c5327310 ...>>, @variant=[]>> to respond to `success?`
     # ./spec/controllers/static_pages_controller_spec.rb:7:in `block (3 levels) in <top (required)>'

Finished in 0.11472 seconds (files took 5.48 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/controllers/static_pages_controller_spec.rb:5 # StaticPagesController#home 正常なレスポンスを返す

Rails6でbe_successメソッドは使えない

Rails6からはbe_successメソッドは使用できなくなっているようです。。
be_successbe_successfulに置き換えるようにしている記事がいくつかありました。

Rails 5.2 betaを使ったRSpec 実行時に be_success へされる非推奨警告

Gist - Replace 'be_success' to 'be_successful'.md

spec/controllers/static_pages_controller_spec.rb
  describe '#home' do
    it "正常なレスポンスを返す" do
      get :home
-       expect(response).to be_success
+       expect(response).to be_successful
    end
  end

再々度テストを実行

やっと通りました。。。

$ rspec spec/controllers/static_pages_controller_spec.rb 

StaticPagesController
  #home
    正常なレスポンスを返す

Finished in 0.04691 seconds (files took 2.47 seconds to load)
1 example, 0 failures

めでたしめでたし。

最後に

これからはコントローラのテストはなくなって、
統合テスト等に置き換わっていくんですよね。

コード量が少なくなるのはいいことですね。

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

2分探索っぽいものを実装した[Ruby]

はじめに

競技プログラミングをやっていたら、小さい順に並んだ無数の自然数の配列中(重複は無い)から、与えられたターゲットが配列のどの数とどの数の間にあるのか実装する必要があった。
無作為に0から探していくと実行時間切れになったので、2分探索っぽい実装をして効率的に探索を行おうとした。

計算量

配列の全ての要素に対して探索を行う場合には、計算量が配列の要素数に比例して大きくなる。つまりオーダー記法で書くとO(N)

これに対して、二分探索を行うと、計算量はO(logN) (2分探索では対数の底は2)となるので、要素数が増えたときにも計算量の増加は比較的少なく済む。

これなら時間内に実行時間内に探索が終わりそう。

実装

とりあえず実装

色々と未熟な点があると思いますので、ご指摘があればコメントいただけるとありがたいです。

array = [1, 4, 6, 8, 20, 34, 87, 118, 234, 769, 1000, 8909] # 自然数の配列(ソート済み)
target = 48 #ターゲットとなる自然数
outputs = "配列の範囲外です"

# targetが配列の範囲内にある場合
if array.first <= target && target <= array.last
  while array.length >= 2 
    length = array.length

   # 配列の中央2つの間にあるか?
    if array[length / 2 - 1] <= target && target <= array[length / 2]
      outputs = array[length / 2 - 1].to_s + " " + array[length / 2].to_s
      break

   # 配列の前半にtargetがあるので配列の前半を切り取る
    elsif target < array[length / 2 - 1 ]
      array = array.take(length / 2)

   # 配列の後半にtargetがあるので配列の後半を切り取る
    else 
      array = array.slice(length / 2 .. length - 1)
    end
  end
end

puts outputs

arrayにソート済みの任意の自然数を、targetに与えられた自然数を代入する。

実行すると以下のようになる。

34 87

確かに48は34と87の間なのでOK

ターゲットが配列の範囲外の場合(target = 10000000とか)には、

配列の範囲外です

と出力される。

まとめ

アルゴリズム関連の知識が乏しいのでこれを機に色々勉強する。
計算量などについても、ぼんやりとしかわからないので調べたものをまたまとめようと思う。

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

renderが省略されてて眠れなくなったので調査

はじめに

この記事はエムスリーキャリア Advent Calendar 2019 18日目の記事です。

エニジニアリンググループのおどです。
新卒入社した会社を8月に辞め、エムスリーキャリアに転職しました。
転職を機にLaravelエンジニアからRailsエンジニアに転身。

今回は利用するフレームワークがLaravelからRuby on Railsに変わり、つまずいたところを軽く深堀してみようと思います。

何に詰まったのか

入社して最初、簡単な文言修正のタスクからアサインされました。

「お、viewファイル見て文字直すだけとか簡単じゃん」

Laravelで開発をしてたときのようにroutingファイルを見て、controllerにたどり着きました。

home_controller.rb
class HomeController < ApplicationController
  def index
  end
end

どのファイルをレンダリングしているんだ…

気になって眠れない日々が始まりました。

Laravelはわかりやすかったな…

HomeController.php
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

class HomeController extends Controller
{
    public function index()
    {
        return view('home.index');
    }
}

調査

binding.pryを使ってトレースしていきたいと思います。

まず、railsではcontrollerのメソッドが終了すると、send_actionというメソッドが呼ばれます。

actionpack-5.2.4/lib/action_controller/metal/basic_implicit_render.rb
# frozen_string_literal: true

module ActionController
  module BasicImplicitRender # :nodoc:
    def send_action(method, *args)
      super.tap { default_render unless performed? }
    end

    def default_render(*args)
      head :no_content
    end
  end
end

controllerのメソッド内でrenderを1度も実行していないと、performed?falseを返します。

basic_implicit_renderをincludeしたaction_controller/metal/implicit_render.rb内に同名のdefault_renderが定義されていて、そこからrenderを呼び出します。

ここで呼ばれるrenderは、自分でcontrollerのメソッド内で呼び出したrenderと同じです。(action_controller/metal/instrumentation.rb)

action_controller/metal/instrumentation.rb
    def render(*args)
      render_output = nil
      self.view_runtime = cleanup_view_runtime do
        Benchmark.ms { render_output = super }
      end
      render_output
    end

さらに追っていくと、abstract_controller/rendering.rb内のrenderメソッドにたどり着きます。

abstract_controller/rendering.rb
    def render(*args, &block)
      options = _normalize_render(*args, &block)
      rendered_body = render_to_body(options)
      if options[:html]
        _set_html_content_type
      else
        _set_rendered_content_type rendered_format
      end
      self.response_body = rendered_body
    end

renderを省略した場合、このrender_to_bodyメソッドに渡る引数が異なってきます。

# renderを省略した場合
[1] pry(#<HomeController>)> options
=> {:prefixes=>["home", "application"], :template=>"index", :layout=>#<Proc:0x00007f481d31fa80@/myapp/vendor/bundle/ruby/2.6.0/gems/actionview-5.2.4/lib/action_view/layouts.rb:392>}

# renderを省略しなかった場合
[1] pry(#<HomeController>)> options
=> {:action=>"index", :prefixes=>["home", "application"], :template=>"index", :layout=>#<Proc:0x00007f481ef20a90@/myapp/vendor/bundle/ruby/2.6.0/gems/actionview-5.2.4/lib/action_view/layouts.rb:392>}

渡される引数を見ると、prefixesに呼び出したコントローラ名、templateにメソッド名が格納されているのが分かります。
renderを省略するとprefixesとtemplateから、省略しないとprefixesとactionからビューファイルの名前を組み立ててレンダリングしてくれているようです。

わかったこと

  • renderを省略すると呼び出しもとからファイル名を組み立ててレンダリングしてくれる

ちなみに

省略した場合に呼ばれるrenderは通常と同じrenderなので、引数だけを省略しても同じ挙動になりました。

home_controller.rb
class HomeController < ApplicationController
  def index
    render
  end
end

感想

たくさんのファイルが継承されていて、覚えられなくて大変でした。
これで安心してrenderを省略でき、ぐっすり眠れそうです。

ご指摘や「ここ間違ってるよ!」、感想など募集中なので気軽にコメントください。

環境

Rails 5.2.4
ruby 2.6.5

参考URL

こちらの記事でもっと詳しく追っていたので、さらに興味がある方はこちらも見てみてください。
Railsはどのようにテンプレートを見つけているか

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

ruby 学習アウトプット1

はじめに

ただの初学者のrubyシルバー取得に向けての学習アウトプットです。
文章もただただ、タコ殴りしてるだけなので、温かい目で見守っていただけると幸いです。

今回の教材

公式擬似問題集

これは下記リンクからgithubに飛びます。
無料です。全50門解説もついてます。(ありがてぇ〜〜〜〜)
https://gist.github.com/sean2121/945035ef2341f0c39bf40762cd8531e0

やってみて

全然だめでした。。。。。
ということで、凹んでられないので詰まった部分を復習していきたいと思います。

以下復習

  • 可変長引数の問題
#下記の実行結果は?
def foo (a, *b)
  p a
end
foo(1,2,3,4)
#結果は
1

*bは可変長引数で引数を複数個設定できる。引数は配列として受け取られるので今回の場合

#*bの中身も出力してみる
def foo (a, *b)
  p a,b
end
foo(1,2,3,4)

#結果
1
[2,3,4]    #bは配列が出力される
  • sizeメソッド
puts({"members" => 193, "year" => 2014}.size)

#結果
2

sizeメソッドは要素の数を返す。今回はハッシュの要素数、配列の場合は配列の要素数。

  • clearメソッド
h = {1=>2, 3=>4}
h.clear
p h   #=>{}

ary = [1, 2]
ary.clear
p ary     #=>[]

#clearメソッドは空のハッシュ、配列を返す

ちなみに問題にはremove,destroy,empty?の選択肢があったのですが、remove,destroyNoMethodErrorempty?はハッシュと配列をそのまま返してました。

  • chopメソッド
#ヘロ〜
s = "hello"
puts s.chop

#しかし!!結果は!!
hell

#地獄!!


chopメソッドは最後の1文字を取り除いた新しい文字列を返しますが、末尾が\r\nで終わってる場合は2文字取り除きます。

ちなみにchompメソッドは末尾の改行コードを取り除きます。
rstripは文字列の末尾の空白文字を除去した新しい文字列を返します。
こんなにあるんか、、、、、、、、と気が滅入り、、、、、いや、がんばります。

  • IOクラスの問題
File.open("foo.txt","r") do |io|
puts io.gets
 puts io.read
  io.rewind
  p lines = io.readlines
end

このコードのio.rewindの処理は何か?って問題でした。
答えはIO#rewindはファイルポインターを先頭に移動させます。とのこと。
つか、IOクラスって何??って思いました。getとかは使っていたけど、IOクラスに関しては全くでした。頼りにしていたチェリー本でも網羅されていませんでした(泣)
なのでIOクラスはしっかり勉強しないとヤバ谷園ですね。()

  • sort
odd = [1,3,5]
even = [2,4,6]
num = odd + even
p num.sort

[1,2,3,4,5,6]

#ちなみにsortをつけないと
[1,3,5,2,4,6]

sortがないときの挙動も確かめときました。

  • 正規表現の問題

/^[hc].*o$/i
この正規表現にマッチするものを2つ選べよ。

A.Hello
B.holland
C.Cello
D.h35L320

こちら先頭の文字がhかc(iオプションで大文字小文字無視)で.*で何かしら文字が連続してoで終わるってことで答えは。。。A,Cだ!!!!
ということなのですが、私、アルファベットのoを数字の0と読み間違えて、ハマってました(泣)

復習してみて

2回目は流石に9割以上解けました。1回目に答えだけでなく1門1門調べて理由まで説明できるようにしました。まぁ、まだまだ、先は長いのでアール。。。。。

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

Rails のバグ報告テンプレートがすごい!

この記事は

ruby-jp の #rails チャンネルで知った Rails のバグ報告テンプレートが目からウロコだった話をします。bug_report_templates/active_record_gem.rb の存在を知らない方向けです。

概要

気軽に ActiveRecord の動作を確かめたい

ちょっと ActiveRecord の動作を確かめたい。でも手元にちょうといい Rails プロジェクトがない。rails new するのは面倒だ……。マイグレーションも書かなきゃ……。

そんなとき、Rails のリポジトリに含まれている次の Ruby スクリプトが役に立ちます。Rails のバグ報告テンプレートだそうです。

https://github.com/rails/rails/blob/v6.0.2/guides/bug_report_templates/active_record_gem.rb

active_record_gem.rb
# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

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

  # Activate the gem you are reporting the issue against.
  gem "activerecord", "6.0.0"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :posts, force: true do |t|
  end

  create_table :comments, force: true do |t|
    t.integer :post_id
  end
end

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

class BugTest < Minitest::Test
  def test_association_stuff
    post = Post.create!
    post.comments << Comment.create!

    assert_equal 1, post.comments.count
    assert_equal 1, Comment.count
    assert_equal post.id, Comment.first.post.id
  end
end

解説

このスクリプトは次のことを行っています。

  • bundler/inline を使って Ruby スクリプト上で bundle install を行っている。
    • gemfile メソッドの第 1 引数に true を渡すことで、まだインストールしていない Gem をインストールする。
    • (Bundler のバージョンが 2.1.0 以上の場合) gemfile メソッドの第 2 引数に quiet: true を渡すと bundle install 時のログ出力を抑制できる。
    • BUNDLE_PATH は無視する。つまり Gem は必ずグローバルにインストールされる。
  • ActiveRecord::Base.establish_connection の database オプションに :memory: を渡すことで、SQLite のデータベースをメモリ上に作成している。
  • ActiveRecord::Schema.define を使うことで、スクリプト内でテーブルを定義、作成している。
  • スクリプト内でモデルを定義している。
  • Minitest::Test で単体テストを実行している。

いろんなテクニックがシンプルに詰まっていて、いいスクリプトだなと思いました :relaxed:

参考

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