【Ruby】配列のsplat展開

調査の背景

現在RailsのAPIモードでバックエンド開発をしており、CORSの設定をするときに環境ごとに許可するオリジンを変えたくAIと壁打ちをするなかで、以下のようなコードに出会った。

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins(*Rails.application.config.x.cors_origins)

    resource "*",
      headers: :any,
      methods: [ :get, :post, :put, :patch, :delete, :options, :head ],
      credentials: true
  end
end

その中の

origins(*Rails.application.config.x.cors_origins)

この部分の引数になぜアスタリスクがついており、どのような働きをするのか知りたかったので調査。

アスタリスクは何をしているか?

これは配列のsplat展開と呼ばれるもので、配列として受け取った引数の中身をひとつひとつ展開して受け取っている。

# メソッドを定義するときに引数につけるアスタリスクは可変長引数。
# これは、受け取った値を一つの配列にまとめることができる。
def sample_method(*args)
  puts args.length
end

list = [ "foo", "bar", "baz" ]

# アスタリスクをつけない場合
sample_method(list)
#=> 1

# アスタリスクをつける場合
sample_method(*list)
#=> 3

今回の場合、development.rbには

config.x.cors_origins = [ "http://localhost:3000" ]

このような定義があるため、この配列が展開されてメソッドの引数として渡されていた。

CSRFとはどのような攻撃でどのように防ぐか?

CSRF(クロスサイトリクエストフォージェリ)について学んだのでざっくりメモ。

CSRFとは?

攻撃者が罠サイトを使って、被害者のブラウザに被害者がログイン状態であることを利用して不正なリクエストをサーバーへ送らせる攻撃。

被害者は自分の銀行サイト(bank.com)で取引を行うためにサイトにログイン。正常な操作を行う。
その後、ログアウトせずに攻撃者のサイト(evil.com)へアクセス。
このサイトでは裏でbank.comへのリクエストを仕込まれており、被害者の気付かぬうちにbank.comのCookieのログイン情報が利用され、それを受け取ったbank.comのサーバーがCookieのログイン情報から正規のリクエストだと認識し、リクエスト通りの操作を実行してしまう、といったことが起きる。

(開発者視点から)どのように防ぐか?

Cookieでセッション管理をしている場合、SameSite属性をLaxにすることで、他サイトからのリクエストでCookieが送信されないようにする。
ただし、リンクなどからのGETリクエストではCookieが送信されてしまうため、そもそもGETリクエストで重要な操作ができないような設計にする。

XSSとはどういう攻撃手法で、どのように防ぐか?

XSS(クロスサイトスクリプティング)についてざっくり学んだのでメモ。

XSSとは何か?

クロスサイトスクリプティング。脆弱性のあるWebサイトに攻撃者がスクリプトを埋め込み、そのスクリプトを実行させる攻撃手法。
例えば、攻撃者のスクリプトが埋め込まれたWebサイトを他のユーザーが訪問する。すると、そのユーザーのブラウザ上で攻撃者が仕込んだスクリプトが実行され、抜き取られたCookie情報が攻撃者のWebサイトに送られてしまうというような被害が起こる。

どのように防ぐか?

CookieをHttpOnlyCookieにすることでJavaScriptからアクセスできないようにする。また、そもそもスクリプトを埋め込ませないことが最重要。htmlやscriptタグのエスケープ処理を適切に行うことで、ブラウザに「実行すべきプログラム」ではなく、「ただ画面に表示するべき文字列」と認識させることで対策ができる。

プロを目指す人のためのRuby入門 第9章

例外は文字通りプログラムの実行中に発生した「例外的な問題」。もっと簡単に言えば「エラーが起きてプログラムの実行を続けることができなくなった状態」とも言える。
例外は補足してプログラムを続行させることもできるし、プログラムを続行できない異常事態として意図的に発生させることもできる。
例外を適切に扱えば堅牢で読みやすいコードが出来上がるし、エラーが起きた場合でも調査しやすくなる。

例外を補足して処理を続行する

例外処理のもっとも単純な構文は以下

begin
    # 例外が起きうる処理
rescue
    # 例外が発生した場合の処理
end
例外オブジェクトから情報を取得する

Rubyでは例外自身もオブジェクトになっている。そのため、例外オブジェクトのメソッドを呼び出すことで発生した例外に関する情報を取得することができる。
messageメソッドは例外発生時のエラーメッセージを返し、backtraceメソッドはバックトレース情報(つまりメソッドの呼び出し履歴)を配列にして返す。
例外オブジェクトから情報を取得したい場合は次のような構文を使う

begin
    # 例外が起きうる処理
rescue => 例外オブジェクトを格納する変数
  # 例外が発生した場合の処理
end

以下コード例

begin
  1 / 0
rescue => e
  puts "エラークラス: #{e.class}"
  puts "エラーメッセージ: #{e.message}"
  puts "バックトレース -----"
  puts e.backtrace
  puts "-----"
end

このコードを実行すると以下のような出力になる。

エラークラス: ZeroDivisionError
エラーメッセージ: divided by 0
バックトレース -----
sample.rb:2:in `/'
sample.rb:2:in `<main>'
-----
クラスを指定して補足する例外を限定する

次のような構文を使って例外のクラスを指定すると、例外オブジェクトのクラスが一致した場合のみ、例外を補足することができる

begin
    # 例外が起きうる処理
rescue 補足したい例外クラス
    # 例外が発生した場合の処理
end

以下のコードはZeroDivisionErrorが発生した場合のみrescue節のコードが実行され、プログラムを続行することができる。ZeroDivisionError以外のエラーは捕捉されず、プログラムが異常終了する。

begin
    1 / 0
rescue ZeroDivisionError
    puts '0で除算しました'
end
#=> 0で除算しました

begin
    # NoMethodErrorが発生する
    'abc'.foo
rescue ZeroDivisionError
    puts '0で除算しました'
end
#=> undefined method `foo' for "abc":String (NoMethodError)
意図的に例外を発生させる

例外はコード中で意図的に発生させることもできる。例外を発生させる場合にはraiseメソッドを使う。
raiseメソッドに文字列だけを渡した場合はRutimeErrorクラスの例外が発生する。
第一引数に例外クラスを、第二引数にエラーメッセージを渡すとRuntimeErrorクラス以外の例外を発生させることができる。

def traffic_light(color)
  case color
  when :blue
    "進む"
  when :yellow
    "注意して止まる"
  when :red
    "止まれ"
  else
    raise ArgumentError, "無効な色です。#{color}"
  end
end

traffic_light(:blue)
#=> "進む"

trafic_light(:green)
#=> 無効な色です。green (ArgumentError)
例外処理のベストプラクティス
  1. 安易にrescueを使わない
    例外はrescue説で補足することで処理を続行できるが、すべての例外をrescueすべきかというとそうではない。むしろ、rescueすべき例外の方が少ない。
    プログラミング初心者は「例外が発生したらrescueで補足すればいいんだな」と考えるのではなく、「例外が発生したら即座に異常終了させよう」もしくは「フレームワークの共通処理に全部丸投げしよう」と考える方が安全。
  2. 例外処理の対象範囲と対象クラスを極力絞り込む
    例外処理を書く場合は、例外が発生しそうな箇所と発生しそうな例外クラスをあらかじめ予想し、その予想を例外処理のコードに反映させる。例外処理の範囲が広すぎたり、補足する例外クラスの種類が多すぎると、本来は異常終了扱いにすべき例外まで続行可能な例外として扱われる恐れがある。
  3. 例外処理よりも条件分岐を使う
    begin ~ rescueを使うよりも条件分岐を使った方が可読性やパフォーマンスの面で有利。例外処理を書く前に公式リファレンスを読んで、問題の有無を事前に確認できるメソッドが用意されていないかチェックする。
    begin/endを省略するrescue修飾子

    rescueは修飾子として使うこともできる。構文は次のようになる。

例外が発生しそうな処理 rescue 例外が発生したときの戻り値

begin/endを省略すると補足する例外クラスは指定できない。例外処理を細かく制御したい場合はrescue修飾子でなく、begin~endを使った例外処理を書く方がよい。

プロを目指す人のためのRuby入門 第8章

モジュールの用途
  • 継承を使わずにクラスにインスタンスメソッドを追加、上書きする(ミックスイン)
  • 複数のクラスに対して共通の特異メソッド(クラスメソッド)を追加する
  • クラス名や定数名の衝突を防ぐために名前空間を作る
  • 関数的メソッドを定義する
  • シングルトンオブジェクトのように扱って設定値などを保持する
    モジュールの定義
# 以下のような構文で定義する
module モジュール名
    # モジュールの定義(メソッドや定数など)
end

クラス構文と似ているが、モジュールからインスタンスを作成することはできない。また、他のモジュールやクラスを継承することもできない。

モジュールを利用したメソッド定義

is-aの関係にないため継承を使うべきではないが、共通の処理を複数のクラスに持たせたい。そんなときにはmoduleに処理を切り出し、クラスの中でincludeすることで複数のクラスで同じ処理が使えるようになる。このようにモジュールをクラスにincludeして機能追加することをミックスインという。

module Greetable
  def say_hello
    puts 'Hello.'
  end
end

class Human
  include Greetable

  def initialize(name)
    @name = name
  end
end

class Animal
  include Greetable

  def initialize(name)
    @name = name
  end
end

human = Human.new("Human")
animal = Animal.new("Animal")

# 両方のインスタンスで同じインスタンスメソッドが使える
human.say_hello
#=> Hello.
animal.say_hello
#=> Hello.

モジュールを利用したメソッド定義のもう一つの方法としてextendがある。extendを使うと、モジュール内のメソッドをそのクラスの特異メソッド(クラスメソッド)にすることができる。

module Greetable
  def say_hello
    puts 'Hello.'
  end
end

class Human
  extend Greetable

  def initialize(name)
    @name = name
  end
end

class Animal
  extend Greetable

  def initialize(name)
    @name = name
  end
end

Human.say_hello
#=> Hello.
Animal.say_hello
#=> Hello.

include, extendはクラス名.include、クラス名.extendの形で呼び出すことも可能。
この章の例題のrainbowメソッドの挙動を理解するために分解しながら動かそうと試していたが、Stringクラスにexampleメソッドを実装したく以下のようなコードを実行したときにエラーが発生。

  def example
    str = self.to_s
  end
  
  p 'Hello, world!'.example
  #=> private method `example' called for "Hello, world!":String (NoMethodError)

なぜこのようなエラーが起きたかというと、Stringクラスにexampleメソッドが実装されていないから。
ちゃんと、module化してStringクラスにincludeすれば、exampleメソッドが使える。

module Example
    def example
        str = self.to_s
    end
end

String.include Example

p 'Hello, world!'.example
#=> "Hello, world!"
名前空間を分けて名前の衝突を防ぐ

モジュール構文の中にクラス定義を書くと「そのモジュールに属するクラス」という意味になるため、同名のクラスがあっても外側のモジュール名さえ異なっていれば名前の衝突は発生しなくなる。
モジュールに属するクラスを参照する場合はモジュール名::クラス名のように::でモジュール名とクラス名を区切る。

module Animal
  class Dog
    def self.speak
      "Woof!"
    end
  end

  class Cat
    def self.speak
      "Meow!"
    end
  end
end

Animal::Dog.speak
#=> "Woof!"

Animal::Cat.speak
#=> "Meow!"
入れ子なしで名前空間付きのクラスを定義する
# この2つは同じこと
module Baseball
  class Second
  end
end

class Baseball::Second
end
モジュール関数の表記法について

モジュール関数は”モジュール名.#メソッド名”と書くことがある。
例えば、KernelモジュールのputsメソッドであればKernel.#putsと表記する。

プロを目指す人のためのRuby入門 第7章

第7章はクラスについて。
クラスは内部にデータを保持し、さらに自分が保持しているデータを利用する独自のメソッドを持つことができる。データとそのデータに関するメソッドが常にセットになるので、クラスを使わない場合に比べてデータとメソッドの整理がしやすくなる。

クラスの定義方法

# クラス名は必ず大文字で始める。キャメルケースで書くのが一般的
class ClassName
end

インスタンスメソッド・クラスメソッドの定義方法

class ClassName
    # インスタンスメソッド(インスタンスに対して呼び出すことのできるメソッド)の定義
    def sample_method
        # 処理
    end
    
    # メソッドの前にself.を付けるとクラスメソッドを定義できる
    def self.sample_method
        # 処理
    end
end

クラスの内部ではインスタンス変数を使うことができる。インスタンス変数は同じインスタンスの内部で共有される変数。@で始まる変数がインスタンス変数になる。@が付かない変数はローカル変数となる。

# インスタンスを作成し、helloメソッドを使おうと思ってもエラーになる。
# nameがインスタンス変数でなく、インスタンス内部で共有できないため。
class User
  def initialize(name)
    name = name
  end

  def hello
    "Hello, I am #{name}."
  end
end
#=> `hello': undefined local variable or method `name' for #<User:0x0000000100dfb8e8> (NameError)

# nameがインスタンス変数のため正しく動作する
class User
  def initialize(name)
    @name = name
  end

  def hello
    "Hello, I am #{@name}."
  end
end
#=> "Hello, I am Alice."

メソッドの表記法
- Rubyではインスタンスメソッドを表す場合に”クラス名#メソッド名”と書くことがある。
- クラスメソッドの場合は”クラス名.メソッド名”(または”クラス名::メソッド名”)と書く。

クラスメソッドをインスタンスメソッドで呼び出す
クラスメソッドをインスタンスメソッドの内部から呼び出す場合はクラス名.メソッド名と書く。

class SampleClass
    def self.example_method(foo)
        # なんらかの処理
    end
    
    def example_method
        # インスタンスメソッドからクラスメソッドを呼び出している
        result = SampleClass.example_method(foo)
    end
end

クラスの継承の書き方

# SubClassはSuperClassを継承している
class SubClass < SuperClass
end

privateメソッド

# privateメソッドはクラスの外からは呼び出せず、クラスの内部でのみ使えるメソッド
class SampleClass
    # publicメソッド。クラスの外部から呼び出せる
    def foo
    end
    
    # ここから下で定義されたメソッドはprivate
    private
    # クラスの内部でのみ使うことができる
    def bar
    end
end

クラス外部からの定数参照方法
クラス名::定数名とすると、クラスの外から定数を参照できる。

# Rubyではメソッド内にスコープを限定した定数を定義できない
# そのためクラス構文の直下で定義する必要がある
class SampleClass
    FOO = "BAR"
end

SampleClass::FOO
#=> "BAR"

入れ子になったクラスの定義

class 外側のクラス
    class 内側のクラス
    end
end

クラス内部に定義したクラスは::を使って参照する

外側のクラス::内側のクラス

プロを目指す人のためのRuby入門 第5章

「プロを目指す人のためのRuby入門」の第5章を読み終わりました。個人的に重要だと思った点をピックアップします。

ハッシュ

ハッシュはキーと値の組み合わせでデータを管理するオブジェクトのこと。他の言語では連想配列やディクショナリ、マップと呼ばれたりする。

# キーと値の組み合わせを3つ格納するハッシュ
{ キー1 => 値, キー2 => 値, キー3 => 値 }

# 改行して書くこともできる
# 配列同様最後にカンマがついてもエラーにはならない
{
    'japan' => 'yen',
    'us' => 'doller',
    'india' => 'rupee',
}

要素の追加、変更、取得

h = {}
# ハッシュ[キー] = 値であとから新しい要素を追加できる
h["name"] = "Alison"
h["position"] = "goalkeeper"
#=> {"name"=>"Alison", "position"=>"goalkeeper"}

# すでにキーが存在していた場合は上書きされる
h["name"] = "Mamardashvili"
#=> {"name"=>"Mamardashvili", "position"=>"goalkeeper"}

# ハッシュ[キー]で値を取り出せる
h["position"] #=> "goalkeeper"

# 存在しないキーを指定するとnilになる
h["number"] #=> nil

ハッシュを使った繰り返し処理

h = { "name"=> "Alison", "position" => "goalkeeper", "number" => 1 }

# eachメソッドを使うとキーと値の組み合わせを順に取り出すことができる。
# キーと値は格納した順番に取り出される。ブロックパラメータはキーと値で2つになる。
h.each do |key, value|
  puts "#{key}: #{value}"
end
#=> name: Alison
#   position: goalkeeper
#   number: 1

# ブロックパラメータを1つにするとキーと値が配列に格納される。
h.each do |key_value|
  p key_value
end
#=> ["name", "Alison"]
#   ["position", "goalkeeper"]
#   ["number", 1]

要素の削除

deleteメソッドを使うと指定したキーに対応する要素を削除できる。戻り値は削除された要素の値。
deleteメソッドで指定したキーがなければnilが返る。
ブロックを渡すと、キーが見つからないときにブロックの戻り値をdeleteメソッドの戻り値とすることができる。

currencies = { 'japan' => 'yen', 'us' => 'doller', 'india' => 'rupee'}

# キーが見つからないので、ブロックの戻り値が返る。
currencies.delete('italy') { |key| "Not found: #{key}" }
#=> "Not found: italy"

# キーが存在するときは、削除された要素の値が返る
currencies.delete('japan') { |key| "Not found: #{key}" }
#=> "yen"

シンボル

シンボルは:シンボルの名前として定義する。
文字列によく似ているが、Rubyの内部では整数として管理されている。

シンボルの特徴と主な用途

ソースコード上では名前を識別できるようにしたいが、その名前が必ずしも文字列である必要でない場合によく使われる。代表的な利用例はハッシュのキー。
ハッシュのキーにシンボルを使うと、文字列よりも高速に値を取り出すことができる。

# キーが文字列
currencies = { 'japan' => 'yen', 'us' => 'doller', 'india' => 'rupee'}
currencies['japan'] #=> 'yen'

# キーがシンボル
# 文字列より高速に取り出せる
currencies = { :japan => 'yen', :us => 'doller', :india => 'rupee'}
currencies[:japan] #=>yen

ハッシュのキーがシンボルになる場合、シンボル: 値という記法でハッシュを作成できる。

currencies = { japan: 'yen', us: 'doller', india: 'rupee'}

ハッシュのキーは同じデータ型である必要はないが、無用な混乱を招くので必要でない限りデータ型は揃えた方がいい。
一方、値に関しては異なるデータ型が混在するケースもよくある。

person = {
    name: "Alice",
    age: 20,
    friends: ["Bob", "Carol"],
    phones: { home: "1234-0000", mobile: "5678-0000" }
}

person[:age]
#=> 20
person[:friends]
#=> ["Bob", "Carol"]
person[:phones][:mobile]
#=> "5678-0000"

ハッシュで使用頻度の高いメソッド

keysメソッドはハッシュのキーを配列として返す。

currencies = { japan: 'yen', us: 'doller', india: 'rupee'}
currencies.keys #=> [:japan, :us, :india]

valuesメソッドはハッシュの値を配列として返す。

currencies = { japan: 'yen', us: 'doller', india: 'rupee'}
currencies.values #=> ['yen', 'doller', 'rupee']

has_key?メソッドはハッシュの中に指定されたキーが存在するかどうか確認するメソッド。戻り値はtrue/false。key?/include?/member?はいずれもエイリアスメソッド。

currencies = { japan: 'yen', us: 'doller', india: 'rupee'}
currencies.has_key?(:japan) #=> true
currencies.has_key?(:italy) #=> false

**をハッシュの前に付けると、ハッシュリテラル内でほかのハッシュの要素を展開できる。mergeメソッドを使っても同じ結果が得られる。

# **を使う場合
h = { us: 'doller', india: 'rupee' }
{ japan: 'yen', **h } #=> {:japan=>"yen", :us=>"doller", :india=>"rupee"}

# mergeメソッドを使う場合
h = { us: 'doller', india: 'rupee' }
{ japan: 'yen' }.merge(h) #=> {:japan=>"yen", :us=>"doller", :india=>"rupee"}

メソッド呼び出し時の{}の省略

Rubyでは、最後の引数がハッシュであればハッシュリテラルの{}を省略できるというルールがある。

# Railsで見るコード
# メソッド呼び出しの括弧、ハッシュリテラルの{}、ハッシュロケットが省略されている
t.string :email, null: false

# 何も省略せず書いた場合
t.string(:nickname, { :null => false })

「最後がハッシュであれば{}は省略可能」というルールは配列リテラルでも同様。

# aとbは同じ構造
a = ['fish', { drink: true, potato: false }]
b = ['fish', drink: true, potato: false ]

ハッシュはto_aメソッドを使うと配列に変換することができる。

currencies = { japan: 'yen', us: 'doller', india: 'rupee'}
currencies.to_a #=>[[:japan, "yen"], [:us, "doller"], [:india, "rupee"]]

配列に対してto_hメソッドを呼ぶと配列をハッシュに変換できる。

currencies = [[:japan, "yen"], [:us, "doller"], [:india, "rupee"]]
currencies.to_h #=> {:japan=>"yen", :us=>"doller", :india=>"rupee"}

%記法でシンボルやシンボルの配列を作成する

# %記法でシンボルを作成する場合は%sを使う
%s!ruby is fun! #=> :"ruby is fun"
%s(ruby is fun) #=> :"ruby is fun"

# %記法でシンボルの配列を作成する場合は%iを使う
%i(apple melon orange) #=> [:apple, :melon, :orange]
%i!apple melon orange! #=> [:apple, :melon, :orange]

# 改行文字、式展開を含める場合は%Iを使う
name = 'Alison'
%I(hello #{name}) #=> [:hello, :Alison]

シンボルと文字列の関係

# to_symメソッドを使うと、文字列をシンボルに変換することができる
"apple".to_sym #=> :apple

# to_sメソッドを使うと、シンボルを文字列に変換することができる
:apple.to_s #=> "apple"