Ruby

はじめに

当ページは.NET(C#)やObjective-Cユーザーの筆者がRuby on Rails (Ruby)を学ぶにあたり書き起こしたものである。

執筆途中であるため、所々にTBDやTODOがある。

Rubyバージョン

Rubyは定期的にリリースされている。バージョン番号を{major}.{minor}.{build}と捉えた場合、以下の傾向が見受けられる。

  • Rubyのマイナーバージョンを毎年12月25日にリリースする
  • マイナーバージョンをリリース後、翌年の2月〜4月にビルドリリース {major.minor.1} をする
  • マイナーバージョン毎にEOLの設定があり、およそ3.25年でEOLを迎える

バージョン履歴

バージョン リリース日 EOL
3.3.0 2023-12-25 TBD
3.2.2 2023-03-30 3.2.0に同じ
3.2.1 2023-02-08 3.2.0に同じ
3.2.0 2022-12-25 2026-03-31(見込み)
3.1.1 2022-02-18 3.1.0に同じ
3.1.0 2021-12-25 2025-03-31(見込み)
3.0.1 2021-04-05 3.0.0に同じ
3.0.0 2020-12-25 2024-03-31(見込み)
2.7.8 2023-03-30 2.7.0に同じ
2.7.1 2020-03-31 2.7.0に同じ
2.7.0 2019-12-25 2023-03-31
2.6.10 2022-04-12 2.6.0に同じ
2.6.1 2019-01-30 2.6.0に同じ
2.6.0 2018-12-25 2022-04-12
2.5.1 2018-03-28 2.5.0に同じ
2.5.0 2017-12-25 2021-04-05
2.4.1 2017-03-22 2.4.0に同じ
2.4.0 2016-12-25 2020-03-31
2.3.1 2016-04-26 2.3.0に同じ
2.3.0 2015-12-25 2019-03-31
2.2.1 2015-03-03 2.2.0に同じ
2.2.0 2014-12-25 2018-03-31
2.1.1 2014-02-24 2.1.0に同じ
2.1.0 2013-12-25 2017-03-31
2.0.0 2013-02-24 2016-02-24

ライフサイクル

Rubyのメンテナンスに関するライフサイクルはRuby ブランチごとのメンテナンス状況 - ruby-lang.orgで示されている。下図は同ページから引用したもの。

./images/figure_ruby_version_lifecycle_01.png

開発環境

Rubyを使うために環境構築をする。

Rubyインストール

Rubyをインストールする方法はいくつかある。

  • パッケージマネージャー

  • インストーラー (Windowsのみ)

  • rbenv & ruby-build
  • Dev Container

rbenv & ruby-build

もしも、複数のRubyのバージョンを使う可能性がある場合にはrbenvを使う手がある。 インストール方法は公式サイトに案内がある。 https://github.com/rbenv/rbenv

上記のサイトからmacOSのインストール方法を抜粋する。

1
brew install rbenv ruby-build

IDE or テキストエディタ

言語基礎

print & p & puts

オブジェクトを標準出力したい場合、 print, p, puts を使う。 各々、以下の違いがある。

  • printは改行コードを含まない(必要な場合は明示的に付与する)
  • pは改行コードを含む(出力する際、テキストがダブルクォートで包まれる)
  • putsは改行コードを含む

ソースコード

1
2
3
4
print "test1"
print "test2\n"
puts "test3"
puts "test4"

結果

1
2
3
test1test2
test3
test4

TODO 文字列と数値

文字列から数値へ

  • 文字列が数字の場合、 to_i メソッドを使うと integer に変換する。
  • 文字列が小数点を含む数字の場合、 to_i メソッドを使うと切り捨てで integer に変換する。
  • 文字列が小数点を含む数字の場合、 to_f で float に変換する。
1
2
3
p "1".to_i   # => 1
p "1.1".to_i # => 1
p "1.1".to_f # => 1.1

数値から文字列へ

1
2
p 1.to_s      # => 1
p (1.1).to_s # => 1.1

真偽値

真偽値について『プロを目指す人のためのRuby入門 改訂2版』のP36に記載がある。以下に引用する。

Rubyの真偽値は次のようなルールを持っています。

  • falseまたはnilであれば偽。
  • それ以外はすべて真。

TODO 配列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
array0 = []
p array0.class

array1 = [1, 3, 5]
p array1

array2 = ["a", "b", "c"]
p array2

array3 = [1, "a", 3, "b", 5, "c"]
p array3
1
2
3
4
Array
[1, 3, 5]
["a", "b", "c"]
[1, "a", 3, "b", 5, "c"]

配列操作

https://ruby-doc.org/3.2.2/Enumerable.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
list = [1,2,3,4,5]

p list
p list.count

#----------
# each
#----------

# inline形式
list.each { |element| p element }

# block形式
list.each do |element|
    p element
end

#----------
# map
#----------

mapped_list_1 = list.map { |element| element * 10 }
mapped_list_2 = list.map do |element|
  element * 10
end

p mapped_list_1
p mapped_list_2

#----------
# select
#----------

select_result = list.select { |element| element % 2 == 0 }
p "select: #{select_result}" # "select: [2, 4]"

#----------
# find
#----------

find_result = list.find { |element| element % 2 == 0 }
p "find: #{find_result}" 

#----------
# any
#----------

any_result = list.any? { |element| element == 3 }
p "any: #{any_result}"

#----------
# sort_by
#----------

sort_by_result = list.sort_by { |element| element }
p "sort_by: #{sort_by_result}"

日付

Rubyの組み込みライブラリないし標準ライブラリは日付や時刻を処理する以下のクラスを提供する。

Time

1
2
3
4
5
6
7
8
time = Time.now
puts time     # => 2023-12-19 17:47:52 +0900
puts time.utc # => 2023-12-19 08:47:52 UTC

# アメリカ東部時間の1997年8月29日午前2時14分を作成する例
# ESTはUTC-5ですが、サマータイム時はUTC-4。
x_day = Time.new(1997, 8, 29, 2, 5, 0, "-04:00")
puts x_day
ISO 8601

APIの応答など日時と時刻を併せ持つ文字列として 2017-07-21T17:32:28Z がある。その文字列書式はISO 8601もしくはRFC 3339と呼ぶ。

書式に関しては以下を参照のこと。

RubyのTimeライブラリは iso8601 メソッドを提供する。前述のように協定世界時の値が欲しい場合は utc メソッドと併せて使う。

1
2
3
require 'time'
time = Time.now.utc
puts time.iso8601
1
2024-01-05T08:25:07Z

以下はローカルタイムの例である。

1
2
require "time"
puts Time.now.iso8601
1
2024-01-05T17:21:37+09:00

TODO Date

TBD.

TODO 演算子

TBD.

自己代入演算子

Rubyには単項演算子のインクリメント演算子 ++ やデクリメント演算子 -- が存在しない。同様なことをするには自己代入演算子を用いる。

1
2
3
4
5
6
7
num = 10
p num += 10 # => 20  // num = num + 10
p num -= 10 # => 10  // num = num - 10

# 少し脱線するが以下のようなこともできる
p num *= 10 # => 100 // num = num * 10
p num /= 10 # => 10  // num = num / 10

条件演算子 (三項演算子)

演算子の再定義 (オーバーロード)

演算子式 (Ruby 3.2 リファレンスマニュアル)によると、演算子の再定義が可能である。

以下の演算子が該当する。

  • |
  • ^
  • &
  • <=>
  • ==
  • ===
  • =~
  • >
  • >=
  • <
  • <=
  • <<
  • >>
  • +
  • -
  • *
  • /
  • %
  • **
  • ~
  • +@
  • -@
  • []
  • []=
  • `
  • !
  • !=
  • !~

ただし、以下の演算子は再定義が不可能である。

  • =
  • ?:
  • ..
  • ...
  • not
  • &&
  • and
  • ||
  • or
  • ::a

TODO ビット演算

  • 論理積 &
  • 論理和 |
  • 排他的論理和 ^
  • 右ビットシフト >>
  • 左ビットシフト <<
  • 論理反転 -

nil

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
array = [1,2]
p array[0] # => 1
p array[1] # => 2
p array[2] # => nil
p array[0].size # => 8
p array[1].size # => 8
begin
    p array[2].size # => NoMethodError
rescue
    p $!
end

# 演算子 &. を使うと、nilオブジェクトにメソッド呼び出し時をしてもエラーが発生せず、nilを返す
p array[2]&.size # => nil

# World1が出力される
if array[2] then
    puts "Hello1"
else
    puts "World1"
end

# Hello2が出力される
if array[2].nil? then
    puts "Hello2"
else
    puts "World2"
end

nil?

Object#nil?はオブジェクトがnilのある場合に true, それ以外の場合に false を返す。 なお、nilオブジェクトのクラスNilClassもnil?メソッドを持つが、常に true を返す。

サンプルコード

1
2
3
4
5
6
7
8
9
def divide(value)
  # 引数valueがnilの場合は0を返す
  return 0 if value.nil?
  value / 2
end

puts divide(4)
puts divide(0)
puts divide(nil)
1
2
3
2
0
0

nil条件演算子

Ruby v2.3.0でSafe navigation operatorが導入された。RubyはNullではなくnilを扱うため、見出しはNull条件演算子ではなくnil条件演算子と記載したが、これは筆者がRubyにおけるSafe navigation operatorに当てられている和名に辿り着けなかったためにそのように勝手に呼んでいることに注意されたい。

サンプルコード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class User
  attr_accessor :name
    
  def initialize(name:)
    self.name = name
  end
end

user1 = User.new(name: "Tanaka")
user2 = nil
user3 = User.new(name: "Terada")
user3.name = nil

if user1.name == "Tanaka"
  puts "Hello #{user1.name}"
else
  puts "Not Found Tanaka"
end

begin
  # user2はnilのためNilClassを参照するが
  # NilClassにはnameメソッドはないため、NoMethodErrorが発生する
  if user2.name == 'Yamada'
    puts "Hello #{user2.name}"
  end
rescue NoMethodError => e
  puts "[ERROR] #{e.class.name}: #{e}"
end

if user2&.name == "Yamada"
  puts "Hello #{user2.name}"
else
  puts "Not Found Yamada"
end

if user3&.name == "Terada"
  puts "Hello #{user3.name}"
else
  puts "Not Found Terada"
end
1
2
3
4
Hello Tanaka
[ERROR] NoMethodError: undefined method `name' for nil:NilClass
Not Found Yamada
Not Found Terada

blank?

TBD.

present?

Ruby on RailsではActive Supportが真偽値を返す present? メソッドを提供する。

以下のケースはいずれも false を返す。それ以外は true を返す。

nil.present?
false.present?
[].present?
{}.present?
"".present?
" ".present?

try

TBD.

TODO ブロック

TBD.

TODO yield

TBD.

https://docs.ruby-lang.org/ja/3.2/doc/spec=2fcall.html#yield

制御構文

制御構文について記す。

if

ifの例を示す。

1
2
3
4
5
6
if 1 < 2
  p 'hoge1'
# elsif foo < bar のように書ける
else
  p 'hoge2' 
end
1
"hoge1"

unless

unlessの例を示す。

1
2
3
4
5
unless 1 < 2
  p 'hoge1'
else
  p 'hoge2' 
end
1
"hoge2"

ifとunlessでガード節

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
require 'pathname'

def is_loadable?(path)
  # 「文字列が空である?」が真なら、偽を返す  
  return false if path.empty?

  # 「ファイルが存在するか?」が偽なら、偽を返す
  return false unless File.exist?(path)
end

path = File.join(File.expand_path("~"), "sample.txt")
p path

p is_loadable?(path)
1
2
"/Users/hiroakit/sample.txt"
false

case

caseの例を示す。 case文には break は用いない。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def check_age_2(age:)
  case age
  when 0 .. 2
    "baby"
  when 3 .. 6
    "little child"
  when 7 .. 12
    "child"
  when 13 .. 18
    "youth"
  else
    "adult"
  end    
end

p check_age_2(age: 18)
1
"youth"

for

forはinの後ろに指定する配列やハッシュの要素数だけ処理を繰り返す。以下に例である。

1
2
3
for i in [1, 2, 3]
  puts i*2
end
1
2
3
2
4
6

while

whileは式が偽を返すまで繰り返し実行する。

1
2
3
4
5
6
7
# コードはhttps://docs.ruby-lang.org/ja/3.2/doc/spec=2fcontrol.html#whileから引用
ary = [0,2,4,8,16,32,64,128,256,512,1024]
i = 0
while i < ary.length
  print ary[i]
  i += 1
end
1
02481632641282565121024

until

untilは式が真を返すまで繰り返し実行する。

1
2
3
4
5
6
7
8
9
# コードは『プロを目指す人のためのRuby入門 改訂2版』のP155から引用

a = [10, 20, 30, 40, 50]

until a.size <= 3
  a.delete_at(-1)
end

p a
1
[10, 20, 30]

next

nextのは最も近いループで次の繰り返し処理に移る。例を示す。 continue に近い。

1
2
3
4
5
6
for i in [1, 2, 3]
  for j in [1, 2, 3]
    next if j.even?
    puts j*2
  end
end
1
2
3
4
5
6
2
6
2
6
2
6

メソッド

引数はメソッド名の後ろに宣言する。

1
2
3
4
5
def sample(message)
  puts message
end

sample "message"

引数を括弧で括らず宣言することもできる。

1
2
3
4
5
def sample message
  puts message
end

sample "message"

戻り値はメソッド内で最後に評価された式になる。メソッド内の処理が戻り値を必ず return で渡しているとは限らない。

1
2
3
4
5
6
def sample
  1 + 1
  "吾輩は猫である" # この行が戻り値である
end

p sample
1
2

そのため、評価対象がない場合は戻り値は nil になる。

1
2
3
4
def sample
end

p sample

メソッド内で評価したものがあっても、 return をする際にオブジェクトを指定していない場合はメソッドは呼び出し元に nil を返す。

1
2
3
4
5
6
def sample()
  1 + 1
  return
end

p sample()

戻り値のオブジェクトやクラスを明示しない1。そのため、関数の実装者と利用者は戻り値の種類は常に確認が必要である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def sample(text)
    if !text
        return "text is nil"
    end
    if text.empty?
        return "text is empty"
    end    

    1 + 1
end

message1 = nil
p sample(message1)

message2 = ""
p sample(message2)

message3 = "123"
p sample(message3)
1
2
3
"text is nil"
"text is empty"
2

クラスのメソッドについてはクラスメソッドとインスタンスメソッドで取り上げる。

クラス

クラスを定義する際に必要となる知識を記す。

クラス定義サンプル

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class SampleClass
  # イニシャライザ。引数を持つことも可能。
  # ただし、オーバーライドはできない。イニシャライザはクラスに1つのみ。
  def initialize()
  end

  # アクセス修飾子の既定はパブリック。
  def sample_method(name:, age:)
    puts "こんにちは #{name} (#{age}歳) さん。"
  end

  # 計算結果を返す場合はreturnを使う。
  def multiple(num)
    return num * 2
  end

  # 慣用的に真偽値を返すタイプのメソッドを示すために使う
  # https://docs.ruby-lang.org/ja/latest/doc/symref.html#q
  def is_ready?
    return false
  end

  # selfの利用例
  def echo
    self.protected_method
    self.private_method_1
  end

  # ifの利用例1
  def check_age_1(age:)
    if age >= 20 then
      puts "成人です"
    elsif age >= 18 then
      puts "#{age}歳は国によっては成人です"      
    else
      puts "未成人です"      
    end
  end

  # ifの利用例2 (ガード節)
  def is_adult?(age:)
    # もしも age が20未満ならば、falseを返す
    return false if age < 20    
    true
  end

  # caseの例 (switch/case)
  def check_age_2(age:)
    case age
    when 0 .. 2
      "baby"
    when 3 .. 6
      "little child"
    when 7 .. 12
      "child"
    when 13 .. 18
      "youth"
    else
      "adult"
    end    
  end
  
  # whileループの例
  def loop_while
    ary = [0,2,4,8,16]
    i = 0
    while i < ary.length
      puts ary[i]
      i += 1
    end
  end

  # forループの例  
  def loop_for
    for i in [1, 2, 3]
      puts i*2
    end
  end

  protected

  def protected_method
    puts 'protected'
  end

  private

  def private_method_1
    puts 'private1'
  end

  # `private` の下で宣言されたメソッドはプライベートになる。
  def private_method_2
    puts 'private2'
  end    
end
1
2
3
4
5
6
7
8
sample = SampleClass.new
sample.sample_method(name: "ジョン", age: 15)
puts sample.multiple(3)
sample.echo
sample.loop_while
sample.loop_for
sample.check_age_1(age: 17)
puts sample.check_age_2(age: 17)

初期化

クラスのインスタンス作成時における初期化について記す。

initialize

クラスのインスタンス作成では初期化処理でクラス内の initialize を呼ぶ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Person
    def initialize(name)
        @name = name      
    end

    def echo()
        puts "hello #{@name}"      
    end
end

person = Person.new("Takeshi")
person.echo() # => hello Takeshi

子クラスのinitializeで super を使い、親クラスのinitializeを呼ぶことも出来る。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Person
    def initialize(name)
        @name = name
    end

    def echo()
        puts "hello #{@name}"
    end
end

class Employee < Person
    def initialize(name)
        super(name)
    end
end

person = Person.new("Takeshi")
person.echo() # => hello Takeshi

employee = Employee.new("Kasumi")
employee.echo() # => hello Kasumi
複数のinitialize

1つのクラスに 複数の initialize がある場合、最後に定義されたinitializeが使われる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Person
    def initialize()
    end

    # このinitalizeが使われる
    def initialize(name)
    end

    def echo()
        puts "hello"
    end
end

# 引数があるinitializeが呼ばれ、ArgumentErrorが発生する
person = Person.new() 
person.echo()

そのため、先ほどのコードで引数なしのinitializeを最後に定義した場合は実行結果に変化が出る。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Person
    def initialize(name)
    end

    # このinitalizeが使われる
    def initialize()
    end

    def echo()
        puts "hello"
    end
end

# 引数がないinitializeが呼ばれ、インスタンス生成が完了する
person = Person.new() 
person.echo() # => hello

変数と定数

Rubyには以下の変数と定数がある。

  1. ローカル変数 … 小文字またはアンダースコアから始まる識別子
  2. インスタンス変数 … @ から始まる識別子
  3. クラス変数 … @@ から始まる識別子
  4. グローバル変数 … $ から始まる識別子
  5. 定数 … アルファベット大文字 ([A-Z]) で始まる識別子
インスタンス変数

インスタンスメソッドから参照、代入が可能。クラスメソッドからは参照できない。initializeメソッドで宣言、初期化する。初期化していないインスタンス変数はnilである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class A    
    def initialize()
        @version = 1
    end

    def print()
        return @version
    end

    def add()
        @version += 1
    end

    def A.print
        return @version
    end    
end
  
class B < A

end

class C < B

end

a = A.new()
p "クラスAのインスタンス: #{a.print()}" # => "クラスAのインスタンス: 1"
p A.print() # => nil

b = B.new()
p "クラスBのインスタンス: #{b.print()}" # => "クラスBのインスタンス: 1"

c = C.new()
p "クラスCのインスタンス: #{c.print()}" # => "クラスCのインスタンス: 1"

a.add() # クラスAのインスタンスaの@versionが2になる

p "クラスAのインスタンス: #{a.print()}" # => "クラスAのインスタンス: 2"
p "クラスBのインスタンス: #{b.print()}" # => "クラスBのインスタンス: 1"
p "クラスCのインスタンス: #{c.print()}" # => "クラスCのインスタンス: 1"

b.add() # クラスBのインスタンスbの@versionが2になる

p "クラスAのインスタンス: #{a.print()}" # => "クラスAのインスタンス: 2"
p "クラスBのインスタンス: #{b.print()}" # => "クラスBのインスタンス: 2"
p "クラスCのインスタンス: #{c.print()}" # => "クラスCのインスタンス: 1"
クラス変数

クラス変数は @@ から始まる識別子である。クラスの特異メソッドもしくはインスタンスメソッドから参照や代入が出来る。クラスの外から直にクラス変数を参照することはできない。

以下のサンプルコードでは @@version がクラス変数に該当する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class A
    @@version = 1

    def print()
        return @@version
    end

    def add()
        @@version += 1
    end    
end
  
class B < A

end

class C < B

end

a = A.new()
puts "クラスAのインスタンス: #{a.print()}" # => クラスAのインスタンス: 1

# 次のようなクラスの外からクラス変数にアクセスする書き方は認められない。SyntaxErrorとなる。
# puts a.@@version

b = B.new()
puts "クラスBのインスタンス: #{b.print()}" # => クラスBのインスタンス: 1

c = C.new()
puts "クラスCのインスタンス: #{c.print()}" # => クラスCのインスタンス: 1

a.add() # @@versionが2になる

puts "クラスAのインスタンス: #{a.print()}" # => クラスAのインスタンス: 2
puts "クラスBのインスタンス: #{b.print()}" # => クラスBのインスタンス: 2
puts "クラスCのインスタンス: #{c.print()}" # => クラスCのインスタンス: 2

b.add() # @@versionが3になる

puts "クラスAのインスタンス: #{a.print()}" # => クラスAのインスタンス: 3
puts "クラスBのインスタンス: #{b.print()}" # => クラスBのインスタンス: 3
puts "クラスCのインスタンス: #{c.print()}" # => クラスCのインスタンス: 3

次のサンプルコードはサブクラスがスーパークラスのクラス変数に影響を及ぼすことを示している。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Base
    @@name = 'Base' # クラス変数
  
    def self.name
      @@name # クラス変数
    end
end

p Base.name # => "Base"

class Product < Base
    @@name = 'Product' # クラス変数
  
    def self.name 
      @@name # クラス変数
    end
end

p Base.name # => "Product"
p Product.name # => "Product"
クラスインスタンス変数

クラスインスタンス変数について以下に言及がある。

Class instance variables are directly accessible only within class methods of the class.

以下に例を示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Base
  @name = 'Base' # クラスインスタンス変数

  def self.name # クラスインスタンス変数
    @name
  end

  def initialize(name)
    @name = name # インスタンス変数
  end

  def name
    @name # インスタンス変数
  end
end

class Photo < Base
  @name = 'Photo' # クラスインスタンス変数

  def self.name
    @name # クラスインスタンス変数
  end
end

class Movie < Base
end

puts Base.name
puts Photo.name
puts Movie.name

b = Base.new("base1")
p = Photo.new("photo1")
m = Movie.new("movie1")

puts b.name
puts p.name
puts m.name
1
2
3
4
5
6
Base
Photo

base1
photo1
movie1

『プロを目指す人のためのRuby入門 改訂2版』のP293ではクラスインスタンス変数を以下のように説明している。

クラスインスタンス変数は同名であってもスーパークラスとサブクラスで異なる変数として参照されます。

どうやら特定のクラスが持つことができる変数のようだ。

定数

定数宣言の例を以下に示す。

1
2
3
4
5
6
class Location
  DEFAULT_LOCATION = "神奈川県町田市"
end

# クラス外部からアクセス可能
p Location::DEFAULT_LOCATION
1
"神奈川県町田市"

定数をプライベートにしたい場合は private_constant を使う。

1
2
3
4
5
6
7
8
class Location
  DEFAULT_LOCATION = "神奈川県町田市"

  private_constant :DEFAULT_LOCATION
end

# NameErrorが発生する
p Location::DEFAULT_LOCATION

メソッド内で定数を宣言することはできない。

1
2
3
4
5
6
7
8
class Location
  DEFAULT_LOCATION = "神奈川県町田市"

  def foo
    # SyntaxErrorになる
    SOME_LOCATION = "あいうえお"
  end
end

なお、Rubyの定数は再代入が可能である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Location
  DEFAULT_LOCATION = "神奈川県町田市"
end

# クラス外部からアクセス可能
p Location::DEFAULT_LOCATION

# 再代入
Location::DEFAULT_LOCATION = "東京都町田市"
p Location::DEFAULT_LOCATION
1
2
"神奈川県町田市"
"東京都町田市"

定数の再代入を止めたい場合はクラスで freeze を使う。(要Pros/Consの検証)

1
2
3
4
5
6
7
8
9
class Location
  DEFAULT_LOCATION = "神奈川県町田市"
  freeze
end

p Location::DEFAULT_LOCATION # => "神奈川県町田市"

# 再代入するとFrozenErrorが発生する
Location::DEFAULT_LOCATION = "東京都町田市"
1
"神奈川県町田市"

TBD: 配列を定数として持つクラスの場合にfreezeの効果を検証する

クラスメソッドとインスタンスメソッド

スタティックメソッドとクラスメソッドはほぼ同義である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class TextEditor
  def initialize()
  end

  def TextEditor.hello_world_1()
    puts "Hello World 1"
  end

  def self.hello_world_2()
    puts "Hello World 2"
  end

  def hello_world_3()
    puts "Hello World 3"
  end 
end

TextEditor.hello_world_1
TextEditor.hello_world_2
editor = TextEditor.new
editor.hello_world_3

プロパティ (getter/setter)

attr_accessor, attr_reader, attr_writerでプロパティ(getter/setter)とインスタンス変数を宣言できる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class TextEditor
  # プロパティ
  attr_accessor :name

  # getter
  attr_reader :text, :version

  # setter
  attr_writer :text

  def initialize()
    @version = "1.0.0"
  end  
end

editor = TextEditor.new
editor.name = "Vim"
editor.text = "Hello World."
p "#{editor.name}"
p "#{editor.version}"
p "#{editor.text}"

継承

クラスおよびモジュールを用いた継承方法について記す。

単一継承 (クラス)

クラスの継承について記す。

次のクラス図を例にコードを起こした。

classDiagram
    class A {
        +String name
        +int age
        +run()
        +echo(text)
    }
    class B {
        +String address
        +run_original()
        +echo()
    }
    A <|-- B

super はオーバーライドしたメソッド内から親クラスにある同名のメソッドを呼び出すことができる。 親クラスの別メソッドを呼び出した場合は NoMethodError が発生する。

次のサンプルコードは以下の特徴がある。

  • クラスBのinitializeで親クラスAのinitializeをsuperで呼び初期化している。
  • クラスBはクラスAのインスタンスメソッドrunをオーバライドしている。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class A
    attr_accessor :name
    attr_accessor :age

    def initialize(name:, age:)
        @name = name
        @age = age
    end

    def run()
        return "#{@name} (#{@age})"
    end

    def echo(text)
        puts text
    end    
end

class B < A 
    attr_accessor :address

    def initialize(name:, age:, address:)
        super(name: name, age: age)
        @address = address
    end

    def run()
        text = super
        return "#{text} @ #{@address}"
    end

    def run_original()
      # super.run() のように親のメソッドを呼ぶことはできない

      # runメソッドはクラスBでオーバーライド済みなので
      # そのメソッドに限りsuper_method.callで呼び出し可
      method(:run).super_method.call       
    end

    # メソッドをオーバーライドする際、引数を意識しないと、継承結果が意図しないものになる
    def echo()
        puts "hoge"
    end    
end

obj1 = A.new(name: "John Doe", age: 9999)
puts obj1.name  # => John Doe
puts obj1.age   # => 9999
puts obj1.run() # => John Doe (9999)
obj1.echo("memo") # => memo

obj2 = B.new(name: "Taro Tanaka", age: 9999, address: "Machida City, Kanagawa")
puts obj2.name    # => Taro Tanaka
puts obj2.age     # => 9999
puts obj2.address # => Machida City, Kanagawa
puts obj2.run()   # => Taro Tanaka (9999) @ Machida City, Kanagawa
puts obj2.run_original() # => Taro Tanaka (9999)
obj2.echo() # => hoge
obj2.echo("memo") # => ArgumentError
TODO 多重継承 (モジュール利用)

TBD.

TODO module

https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#module

TODO 特異クラス定義

https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#singleton_class

TODO 構造体クラス

Rubyの構造体はクラスのようである。 https://docs.ruby-lang.org/ja/latest/class/Struct.html

そのためRubyの構造体は値型(メモリのスタック領域を使う)なのか 参照型(メモリのヒープ領域を使う)なのかがわからない。

名前空間

クラスに名前空間を設けたい場合、モジュールが有効である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
module Module1
  class Product
    def initialize
      p "Module1::Product"
    end
  end

  class PhotoAlbum
    def initialize
      p "Module1::PhotoAlbum"
    end
  end
end

module Module1
  class Book
    def initialize
      p "Module1::Book"
    end
  end
end

class Module1::Article
  def initialize
    p "Module1::Article"    
  end
end

module Module2
  class PhotoAlbum
    def initialize
      p "Module2::PhotoAlbum"
    end
  end
end

product = Module1::Product.new()
book = Module1::Book.new()
album1 = Module1::PhotoAlbum.new()
album2 = Module2::PhotoAlbum.new()
article = Module1::Article.new()
p product.class
p book.class
p album1.class
p album2.class
p article.class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"Module1::Product"
"Module1::Book"
"Module1::PhotoAlbum"
"Module2::PhotoAlbum"
"Module1::Article"
Module1::Product
Module1::Book
Module1::PhotoAlbum
Module2::PhotoAlbum
Module1::Article

ファイル操作

ファイルを開く

テキストファイル
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
file_name = "testfile.txt"

# ファイルを書き込みモードで開く。ファイルがある場合は内容を空にする。ファイルがない場合は作成する。
f1 = File.open(file_name, "w")
p f1.class # => File
f1.print "test"
f1.close

# ファイルの中身を出力する
p File.read(file_name)

# 読み込みモードでファイルを開く
begin
    # 第2引数を省略すると読み込みモード。明示したい場合は第2引数に "r" を指定する。
    f2 = File.open(file_name)

    # 読み込みモードで開いたファイルに書き込もうとするとIOErrorを起こす。
    f2.print "test"

    # このサンプルは意図的にエラーを起こしているため
    # ファイルをクローズする次のコードには到達しない
    f2.close
rescue IOError => error
    p error

    # エラーをキャッチしたタイミングではファイルが開きっぱなし
    p f2 # => #<File:testfile.txt>

    # ファイルを閉じる
    f2.close
    p f2 # => #<File:testfile.txt (closed)>
end

# 存在しないファイルを読み込みモードで開こうとした場合はErrno::ENOENTを起こす
begin
    f3 = File.open("sample_12345.txt", "r")
    f3.close
rescue Errno::ENOENT => error
    p error
end

begin
    # ファイルを書き込みモードで開く。ファイル末尾に書き込む。ファイルがない場合は作成する。
    # なお、ブロック構文を使う場合、ブロックの実行が終了すると、ファイルは自動的にクローズされる。
    File.open("aiueo.txt", "a") do | file |
        file.print "test12345"
    end      
rescue 
    p $!
end
バイナリファイル

バイナリファイルの例として画像ファイルを開く。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
begin
    # Windowsの場合はテキストorバイナリでファイルを区別するため。
    # File.openではbフラグを用いてr+bやw+bと指定する必要がある。
    # 以下のは第2引数はmacOSの場合で実行されることを想定して記述した。
    File.open("sample.png", "r") do | src |
        puts src.size
        File.open("copy.png", "w") do | dst |
            dst.write(src.read)
        end        
    end
rescue 
    p $!
end

ファイルの存在確認

File.exist?はFileText.exist?と同じである。

1
2
3
4
file_name = "testfile.txt"

puts File.exist?(file_name) # => true
puts File.exist?("aaaa") # => false

ファイルパスの作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
require 'pathname'

# ファイルパスの作成
p File.join("", "Users", "foo")     # => "/Users/foo"

# ファイルパスのオブジェクト
p Pathname.new("foo/bar")   # => #<Pathname:foo/bar>
p Pathname("foo/bar")       # => #<Pathname:foo/bar>
path = Pathname("/tmp")     # => #<Pathname:/tmp>
path += "hoge"
p path              # => "/tmp/hoge"
p File.path("/tmp") # => "/tmp"
p File.path(path)   # => "/tmp/hoge"

p Dir.getwd                         # => 現在のディレクトリまでのパスを出力する。 (例: "/Users/hiroakit/Sample")

# 絶対パスに変換する
p File.absolute_path(".")           # => Dir.getwdと同じ結果を出力する。 (例: "/Users/hiroakit/Sample")
p File.absolute_path("..")          # => 1つ上の階層のパスを出力する。 (例: "/Users/hiroakit")
p File.absolute_path("..", "/tmp")  # => 第二引数は基準となるディレクトリを指定できる。"/"
p File.absolute_path("~")           # => チルダをホームディレクトリとしては展開しない。 (例: "/Users/hiroakit/Sample/~")

# 絶対パスであるか評価する。真は絶対パス。偽はそれ以外。
# OSのプラットフォームによって真偽が異なる。以下はmacOSでの実行結果になる。
p File.absolute_path?(".")    # => false
p File.absolute_path?("..")   # => false
p File.absolute_path?("~")    # => false
p File.absolute_path?("/tmp") # => true

# 絶対パスに展開する
puts "File.expand_path ======="
p File.expand_path(".")          #=> "/Users/hiroakit/Sample"
p File.expand_path("..")         #=> "/Users/hiroakit"
p File.expand_path("..", "/tmp") #=> "/"
p File.expand_path("~")          #=> "/Users/hiroakit"
begin
    # ユーザーfooが存在するなら"/Users/foo"
    # 存在しない場合はArgumentErrorが発生する
    p File.expand_path("~foo")
rescue 
    p $!
end

# パスからファイル名のみを取り出す
p File.basename("test/sample.txt")     # => "sample.txt"
p File.basename("test/archive.tar.gz") # => "archive.tar.gz"
p File.basename(".gitignore")          # => ".gitignore"

JSON

https://docs.ruby-lang.org/ja/3.2/library/json.html https://docs.ruby-lang.org/ja/latest/class/JSON.html

JSONファイルを読み取るサンプル

JSONサンプル

1
2
3
4
{
    "id" : 1,
    "name" : "john"
}

JSONファイルを読み取るサンプル

1
2
3
4
5
6
7
8
require "json"

arr = []
File.open("data.json") do |json|
  hash = JSON.load(json)
  arr.push(hash)
end
p arr # [{"id"=>1, "name"=>"john"}]

TODO YAML

テキストエンコーディング

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 文字エンコードの定義
# 詳細は https://docs.ruby-lang.org/ja/3.2/class/Encoding.html を参照
p Encoding::SJIS.name       # => "Windows-31J"
p Encoding::EUC_JP.name     # => "EUC-JP"
p Encoding::UTF_8.name      # => "UTF-8"
p Encoding::UTF_16.name     # => "UTF-16"

# UTF-8のテキストファイル
puts "utf8.txt"
File.open("./sample/utf8.txt", "r") do | file |
    file.each do | line |
        p "#{file.lineno}: #{line}"
    end
end

# Shift JISのテキストファイル
# 文字コードを指定する場合は、ファイルから読み取るの時の文字コード、標準出力向けの文字コードを指定する
puts "sjis.txt"
File.open("./sample/sjis.txt", "r:#{Encoding::SJIS.name}:#{Encoding::UTF_8.name}") do | file |
    file.each do | line |
        p "#{file.lineno}: #{line}"
    end
end

# EUC-JPのテキストファイル
puts "eucjp.txt"
File.open("./sample/eucjp.txt", "r:#{Encoding::EUC_JP.name}:#{Encoding::UTF_8.name}") do | file |
    file.each do | line |
        p "#{file.lineno}: #{line}"
    end
end

# UTF-8 BOMありのテキストファイル
puts "utf8bom.txt"
File.open("./sample/utf8bom.txt", "r") do | file |
    file.each do | line |
        p "#{file.lineno}: #{line}"
    end
end

# Shift JISのテキストファイルを読み取る
sjis_text = File.read('./sample/sjis.txt', encoding: Encoding::SJIS)
puts sjis_text.encoding
puts sjis_text.encode(Encoding::UTF_8) # 外部エンコーディングを指定していないため、ここで変更

# Shift JISのテキストファイルを読み取る。内部、外部のエンコーディングを指定する
sjis_text2 = File.read('./sample/sjis.txt', encoding: "#{Encoding::SJIS}:#{Encoding::UTF_8}")
puts sjis_text2

TODO スレッド

プログラム実行開始時、1つスレッドが作成され、それをメインスレッドとして用いる。

1
2
p Thread.main # => #<Thread:0x000000010059b158 run>
p Thread.list # => [#<Thread:0x000000010059b158 run>] # スレッドは1点のみ。

スレッドを立てるにはThread.newThread.startやThread.forkを使う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def time1
    sleep(2)
    puts "time1 #{Thread.current} #{Time.now.strftime('%H:%M:%S')}"
end
  
def time2    
    sleep(2)
    puts "time2 #{Thread.current} #{Time.now.strftime('%H:%M:%S')}"
end

threads = []
threads.push(Thread.new { time1() })
threads.push(Thread.new { time2() })
threads.push(Thread.new { time1() })
threads.push(Thread.new { time2() })
threads.each {|t| t.join}

# 実行結果
# マルチスレッドの処理なので、この順番通りに出力されるわけではない。
# 
# time2 #<Thread:0x000000010315a780 sample_2.rb:15 run> 18:03:26
# time1 #<Thread:0x000000010315aaa0 sample_2.rb:12 run> 18:03:26
# time2 #<Thread:0x000000010315a960 sample_2.rb:13 run> 18:03:26
# time1 #<Thread:0x000000010315a870 sample_2.rb:14 run> 18:03:26

async/await

言語構文にasync/awaitの定義は見受けられなかった。 サードパーティのGemパッケージにasync/awaitに関するものがある。 本ページのasync / awaitで取り上げる。

例外処理

例外のオブジェクトは以下のものがある。

  • RuntimeError
  • SyntaxError
  • StandardError

公式ドキュメント

サンプルコード

クラスやメソッドの外で例外処理をする場合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
begin
    1 / 0    
rescue ZeroDivisionError => error
    puts $!.class
    
    begin
        open("nonexistent file")
    rescue Errno::ENOENT => error
        puts "場所1"
        puts error.class
        puts $!.class

        raise SyntaxError.new("invalid syntax") # ここは誰も拾わない
    ensure
        puts "場所2"
    end
rescue StandardError => error
    puts "hogehoge"
    puts $!
    puts error
rescue
    puts "hoge"
    puts $!.class
ensure
    puts "場所3"
end

クラスに定義したメソッドで例外処理をする場合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Hoge
    def divide_1()      
        1 / 0
    end

    def divide_2()        
        ret = 0
        ret = 1 / 0
        rescue
            puts "hoge1"
        else
            puts "hoge2"
        ensure
            puts "hoge3"
            return ret
    end
end

hoge = Hoge.new
puts hoge.divide_1 rescue puts "hoge0"
puts hoge.divide_2 rescue puts "hoge4"

TODO 正規表現

TBD.

TODO 命名規則

TBD.

TODO ドキュメントコメント

TBD.

TODO RDOC

TBD.

TODO YARD

TBD.

HTTP

HTTPクライアント、HTTPリクエスト、HTTPレスポンスなどのオブジェクトを用いたHTTPプロトコル上のデータ送受信方法について記す。

サンプルコードは通信先にJSONPlaceholderを用いる。

net/http

Rubyは標準添付ライブラリ net/http を持つ。 同ライブラリが提供する以下のオブジェクトでデータ送受信の実装をできる。

公式ドキュメント

HTTP GETの例

以下にHTTP GETリクエストを送り、HTTPレスポンスをオブジェクトとして受け取る例を示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require 'net/http'
require 'uri'

uri = URI.parse("https://jsonplaceholder.typicode.com/todos/1")

# HTTPリクエスト作成
request = Net::HTTP::Get.new(uri.path)

# HTTPクライアント作成
http = Net::HTTP.new(uri.host, uri.port) # この段階ではTCPコネクションを作らない
http.use_ssl = uri.scheme === "https" # HTTPSを使う場合はuse_sslをtrueにする

# HTTPクライアントからHTTPリクエストを送りHTTPレスポンスを受け取る
# TCPコネクションは自動で作成され、破棄される(要確認)
response = http.request(request)

puts response.code
# => 200

puts response['content-type']
# => application/json; charset=utf-8

puts response.body
# =>
# {
#   "userId": 1,
#   "id": 1,
#   "title": "delectus aut autem",
#   "completed": false
# }

なお、URIのスキーマがHTTPSの場合はNet::HTTPのインスタンスメソッド use_ssl を真にする必要がある。そのため、 Net::HTTP.get でHTTPSのURIに対してリクエストを送ると、以下のように~SocketError~ が発生する。

1
2
3
4
require 'net/http'

print Net::HTTP.get("https://jsonplaceholder.typicode.com", "/todos/1", 443)
# => Failed to open TCP connection to https://jsonplaceholder.typicode.com:443 (getaddrinfo: nodename nor servname provided, or not known) (SocketError)

HTTP POSTの例

以下にHTTP POSTリクエストを送り、HTTPレスポンスをオブジェクトとして受け取る例を示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
require 'net/http'
require 'uri'
require "json"

uri = URI.parse("https://jsonplaceholder.typicode.com/posts")

# Content-Typeを未指定の場合 "application/x-www-form-urlencoded" となる
headers = { "Content-Type" => "application/json; charset=UTF-8" } 

# HTTPボディに付与するJSON
params = { title: "John Doe" }

# HTTPリクエスト作成
request = Net::HTTP::Post.new(uri.path, headers)
request.body = params.to_json

# HTTPクライアント作成
http = Net::HTTP.new(uri.host, uri.port) # この段階ではTCPコネクションを作らない
http.use_ssl = uri.scheme === "https" # HTTPSを使う場合はuse_sslをtrueにする

# HTTPクライアントからHTTPリクエストを送りHTTPレスポンスを受け取る
# TCPコネクションは自動で作成され、破棄される(要確認)
response = http.request(request)

# response = http.post(uri.path, params.to_json, headers)

puts response.code
# => 201

puts response['content-type']
# => application/json; charset=utf-8

puts response.body
# =>
# {
#  "title": "John Doe",
#  "id": 101
# }

TODO httparty

https://github.com/jnunemaker/httparty

TODO rest-client

https://github.com/rest-client/rest-client

async / await

Socketry async

サンプルコード

Gemfile

1
2
3
source 'https://rubygems.org'
gem 'async', '~> 2.6', '>= 2.6.5'
gem 'async-http', '~> 0.61.0'

sample.rb

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
require 'async'
require 'async/http/internet'

Async do |task|
	puts "Hello World!"
end

task_1 = Async do
	rand
end
puts "The number was: #{task_1.wait}"

#
# Syncを用いたHTTP GETリクエスト
#
def fetch(url)
	Sync do
		internet = Async::HTTP::Internet.new
		return internet.get(url).read
	end
end

Sync do
    puts fetch('https://www.hiroakit.com')
end

task_2 = Async do
    fetch('https://www.hiroakit.com')
end
puts "#{task_2.wait}"

#
# Asyncを用いたHTTP GETリクエスト
#
def fetch_async(url)
	Async do | task |
		internet = Async::HTTP::Internet.new
		internet.get(url).read
	end
end
puts fetch_async('https://www.hiroakit.com').wait

task_3 = fetch_async('https://www.hiroakit.com')
puts "#{task_3.wait}"

Delayed::Job

https://www.hiroakit.com/archives/20240310/

TODO GraphQL

https://graphql-ruby.org

TODO SQL

TBD.

MessagePack - msgpack

RubyでMessagePackを使う場合はGemパッケージ msgpack を使う。

インストール

コマンドラインでインストールする。

1
gem install msgpack

もしくはGemfileに書き、 bundle install をする。

1
2
3
source 'https://rubygems.org'

gem 'msgpack', '~> 1.7', '>= 1.7.2'

簡単な動作確認をする。

1
2
3
4
5
6
require 'msgpack'
msg = [1,2,3].to_msgpack
unpacked = MessagePack.unpack(msg)

p msg  #=> "\x93\x01\x02\x03"
p unpacked #=> [1,2,3]

自作クラスをMessage Packにする

MessagePackはExtension types仕様があり、そこでアプリケーション固有の型について述べている。

MessagePack allows applications to define application-specific types using the Extension type. Extension type consists of an integer and a byte array where the integer represents a kind of types and the byte array represents data.

Applications can assign 0 to 127 to store application-specific type information. An example usage is that application defines type = 0 as the application's unique type system, and stores name of a type and values of the type at the payload.

ここではアプリケーション固有の型を自作クラスと呼ぶが、Gemパッケージ msgpack もExtension typesをサポートしているため、自作クラスをMessagePackのシリアライズ、デシリアライズが出来る。

その実装方法について msgpackREADMEのExtension typesで言及があるが、筆者は理解が追いつかなかったため、以下にサンプルコードを用意した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
require 'msgpack'

class Base
    def initialize(name, nick_name = nil)
      @name = name
      @nick_name = nick_name
    end

    attr_reader :name, :nick_name

    def to_msgpack_ext
        [name, nick_name].to_msgpack      
    end

    def self.from_msgpack_ext(data)
        self.new(*MessagePack.unpack(data))
    end
end

MessagePack::DefaultFactory.register_type(0x01, Base)

b = Base.new("base1", "aaa")
b_msg = MessagePack.pack(b)
b_unpacked = MessagePack.unpack(b_msg)

p b # => #<Base:0x0000000100dca338 @name="base1", @nick_name="aaa">
p b_msg # => #"\xC7\v\x01\x92\xA5base1\xA3aaa"
p b_unpacked # => #<Base:0x0000000100dc9b68 @name="base1", @nick_name="aaa">
p b_unpacked.name # => #"base1"
p b_unpacked.nick_name # => #"aaa"

要点は以下の通りである。

  • MessagePack::DefaultFactory.register_type で自作クラスを登録する。
  • 自作クラスには to_msgpack_extfrom_msgpack_ext を実装する。
  • シリアライズする際は MessagePack.pack を用いる。 to_msgpack_ext が呼ばれる。
  • デシリアライズする際は MessagePack.unpack を用いる。 from_msgpack_ext が呼ばれる。

前述のサンプルコードは以下のコードを基に幾許かの推測を加えた。

TODO テスト

TBD.

TODO Reactive Extention - RxRuby

TBD.

TODO Dependency Injection - dry-rb

TBD.

Ruby on Rails

Ruby on RailsはModel-View-Controllerアーキテクチャのウェブアプリケーションフレームワークである。David Heinemeier Hansson氏が2005年12月にバージョン1.0をリリースした。現在の最新メジャーバージョンは7である。

TODO 活用事例

TBD.

Railsバージョン

出典: https://rubyonrails.org/category/releases

バージョン 日付
7.1.2 2023年11月10日
7.1 2023年10月5日
7.0 2021年12月15日
6.1.5 2022年3月10日
6.0 2019年10月31日
5.2.7 2022年3月11日
5.0 2016年6月30日
4.0 2013年6月25日
3.0 2010年8月29日
2.0 2007年12月7日
1.0 2005年12月13日

Railsプロジェクト作成

簡易な作成方法

簡易な方法を記載する。

筆者の動作環境を以下に示す。

1
2
3
4
5
sw_vers
which -a ruby
ruby --version
which -a rbenv
rbenv --version
1
2
3
4
5
6
7
8
ProductName:		macOS
ProductVersion:		14.4
BuildVersion:		23E214
/Users/hiroakit/.rbenv/shims/ruby
/usr/bin/ruby
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin21]
/Users/hiroakit/.rbenv/bin/rbenv
rbenv 1.2.0-62-ga632465

作業ディレクトリとして ${TMPDIR}rails-playground を作成する。

1
2
mkdir -p ${TMPDIR}rails-playground
cd ${TMPDIR}rails-playground

${TMPDIR}rails-playgroundGemfile を作成する。

1
2
3
4
5
6
7
cat << _EOT_ > Gemfile
source "https://rubygems.org"

ruby "3.2.2"

gem "rails", "7.1.2"
_EOT_

Bundler をインストールして、 bundle install を実行する。

1
2
gem install bundler
bundle install

以下に実行結果を示す。

1
2
3
4
5
6
Successfully installed bundler-2.5.6
Parsing documentation for bundler-2.5.6
Done installing documentation for bundler after 0 seconds
1 gem installed
Bundle complete! 1 Gemfile dependency, 58 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

Railsアプリケーションプロジェクトを作成する。

1
2
rails new app1 --minimal
cd app1

以下に実行結果を示す。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
Based on the specified options, the following options will also be activated:

  --skip-active-job [due to --minimal]
  --skip-action-mailer [due to --skip-active-job, --minimal]
  --skip-active-storage [due to --skip-active-job, --minimal]
  --skip-action-mailbox [due to --skip-active-storage, --minimal]
  --skip-action-text [due to --skip-active-storage, --minimal]
  --skip-javascript [due to --minimal]
  --skip-hotwire [due to --skip-javascript, --minimal]
  --skip-action-cable [due to --minimal]
  --skip-bootsnap [due to --minimal]
  --skip-dev-gems [due to --minimal]
  --skip-jbuilder [due to --minimal]
  --skip-system-test [due to --minimal]

      create  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  .gitattributes
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /private/var/folders/zs/f7xjhm_157959t0chxsmzz900000gn/T/rails-playground/app1/.git/
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/stylesheets/application.css
      create  app/channels/application_cable/channel.rb
      create  app/channels/application_cable/connection.rb
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/assets/images
      create  app/assets/images/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/rails
      create  bin/rake
      create  bin/setup
      create  Dockerfile
      create  .dockerignore
      create  bin/docker-entrypoint
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/puma.rb
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
      create  config/environments/test.rb
      create  config/initializers
      create  config/initializers/assets.rb
      create  config/initializers/content_security_policy.rb
      create  config/initializers/cors.rb
      create  config/initializers/filter_parameter_logging.rb
      create  config/initializers/inflections.rb
      create  config/initializers/new_framework_defaults_7_1.rb
      create  config/initializers/permissions_policy.rb
      create  config/locales
      create  config/locales/en.yml
      create  config/master.key
      append  .gitignore
      create  config/boot.rb
      create  config/database.yml
      create  db
      create  db/seeds.rb
      create  lib
      create  lib/tasks
      create  lib/tasks/.keep
      create  lib/assets
      create  lib/assets/.keep
      create  log
      create  log/.keep
      create  public
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/apple-touch-icon-precomposed.png
      create  public/apple-touch-icon.png
      create  public/favicon.ico
      create  public/robots.txt
      create  tmp
      create  tmp/.keep
      create  tmp/pids
      create  tmp/pids/.keep
      create  tmp/cache
      create  tmp/cache/assets
      create  vendor
      create  vendor/.keep
      create  test/fixtures/files
      create  test/fixtures/files/.keep
      create  test/controllers
      create  test/controllers/.keep
      create  test/mailers
      create  test/mailers/.keep
      create  test/models
      create  test/models/.keep
      create  test/helpers
      create  test/helpers/.keep
      create  test/integration
      create  test/integration/.keep
      create  test/channels/application_cable/connection_test.rb
      create  test/test_helper.rb
      create  storage
      create  storage/.keep
      create  tmp/storage
      create  tmp/storage/.keep
      remove  app/jobs
      remove  app/views/layouts/mailer.html.erb
      remove  app/views/layouts/mailer.text.erb
      remove  app/mailers
      remove  test/mailers
      remove  app/javascript/channels
      remove  app/channels
      remove  test/channels
      remove  config/initializers/cors.rb
      remove  config/initializers/new_framework_defaults_7_1.rb
         run  bundle install
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Bundle complete! 6 Gemfile dependencies, 63 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
         run  bundle lock --add-platform=x86_64-linux
Writing lockfile to /private/var/folders/zs/f7xjhm_157959t0chxsmzz900000gn/T/rails-playground/app1/Gemfile.lock
         run  bundle lock --add-platform=aarch64-linux
Writing lockfile to /private/var/folders/zs/f7xjhm_157959t0chxsmzz900000gn/T/rails-playground/app1/Gemfile.lock
         run  bundle binstubs bundler

アプリケーションを立ち上げて http://localhost:3000 にアクセスする。

1
2
cd app1
bin/rails server

DevContainerを用いる方法

作業ディレクトリとして ${TMPDIR}rails-playground/rails-devcontainer-sample を作成する。

1
2
mkdir -p ${TMPDIR}rails-playground/rails-devcontainer-sample
cd ${TMPDIR}rails-playground/rails-devcontainer-sample

.devcontainer を作成する。

1
mkdir .devcontainer

.devcontainerdevcontainer.json を作成する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cat << _EOT_ > .devcontainer/devcontainer.json
{
  "image": "mcr.microsoft.com/devcontainers/ruby:3",
  "hostRequirements": {
    "cpus": 4
  },
  "waitFor": "onCreateCommand",
  "updateContentCommand": "bundle install",
  "postCreateCommand": "",
  "postAttachCommand": {
    "server": "rails server"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "Shopify.ruby-lsp"
      ]
    }
  },
  "portsAttributes": {
    "3000": {
      "label": "Application",
      "onAutoForward": "openPreview"
    }
  },
  "forwardPorts": [3000]
}
_EOT_

Gemfile${TMPDIR}rails-playground/rails-devcontainer-sample に作成する。

1
2
3
4
5
6
7
cat << _EOT_ > Gemfile
source "https://rubygems.org"

ruby "3.3.0"

gem "rails", "7.1.3"
_EOT_

Visual Studio Code (以下、VSCode)で開く。

code .

VS Codeでリモートコンテナーを開くと devcontainer.jsonupdateContentCommand に従って bundle install が実行される。

rm Gemfile Gemfile.lock
rails new . --minimal

アプリケーションを立ち上げて http://localhost:3000 にアクセスする。

rails server

番外編 - Bundlerでインストール先をvendor/bundleに指定する

BundlerでGemパッケージをインストールする際にインストール先を指定できるオプションがあるが、このオプションを使うとBundler v2.5.6 では以下の通り --path ではなく bundle config set path vendor/bundle を使うことを推奨する。

$ bundle install --path vendor/bundle
[DEPRECATED] The `--path` flag is deprecated because it relies on being remembered
across bundler invocations, which bundler will no longer do in future versions.
Instead please use `bundle config set path 'vendor/bundle'`, and stop using this flag

<中略>

以下は bundle install --path vendor/bundle を使うケースである。この方法は作業ディレクトリを設けて、そこにRailsプロジェクト作成専用のRailsをインストールする。

作業ディレクトリに移動する。

1
mkdir foo && cd foo

Gemfileを作成する。

1
2
3
4
5
6
7
cat << _EOT_ > Gemfile
source "https://rubygems.org"

ruby "3.2.2"

gem "rails", "7.1.2"
_EOT_

Railsインストール用のステップとしてBundlerコマンドを実行する。

1
2
gem install bundler
bundle install --path vendor/bundle

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

1
bundle exec rails new example --skip-bundle

後処理としてRailsインストール用のステップでインストールしたRailsを削除する。

1
2
3
rm -f Gemfile Gemfile.lock
rm -rf .bundle
rm -rf vendor

データベース接続

rails new でデータベースを指定しなかった場合、SQLite 3を使う。

MySQL

RailsでMySQLに接続する場合はGemパッケージ mysql2 を使う。

Gemfileに以下の記述を加える。(執筆時では0.5.6が最新)

1
gem "mysql2", "0.5.6"
macOSでの諸注意

macOS (Sonoma 14.4) でGemパッケージ mysql2 をインストールする場合は、以下のコマンド実行が必要な可能性がある。

1
2
3
4
5
brew install openssl@1.1 zstd
bundle config --local build.mysql2 -- --with-openssl-dir=$(brew --prefix openssl@1.1)

export LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix zstd)/lib/
bundle install

scaffold

rails generate scaffold を実行すると、コントローラー、モデル、ビュー、ルーティング、テストコードを作成する。

ye#+begin_src :eval no rails generate scaffold User name:string rails db:migrate

#+end_src

なお、 db:migrate を実行していない場合は ActiveRecord::PendingMigrationError が発生する。

ルーティング

ルーティングについて『Ruby on Rails 7 ポケットリファレンス』のP368に記載がある。以下に引用する。

ルーティングとは、リクエストURLに応じて処理の受け渡し先を決定すること、またはそのしくみのことをいいます。Railsでは、クライアントからの要求を受け取ると、まずはルーティングを利用して呼び出すべきアクションを決定します。

ルーティング設定の基礎

ルーティング設定は config/routes.rb に記述する。以下に例を示す。

1
2
3
Rails.application.routes.draw do
  resources :articles
end

ルーティングはコマンド bin/rails routes で確認できる。以下に例を示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ bin/rails routes
      Prefix Verb   URI Pattern                  Controller#Action
    articles GET    /articles(.:format)          articles#index
             POST   /articles(.:format)          articles#create
 new_article GET    /articles/new(.:format)      articles#new
edit_article GET    /articles/:id/edit(.:format) articles#edit
     article GET    /articles/:id(.:format)      articles#show
             PATCH  /articles/:id(.:format)      articles#update
             PUT    /articles/:id(.:format)      articles#update
             DELETE /articles/:id(.:format)      articles#destroy

パラメーター制約

例えば、以下のように article が受付可能なパラメーターを定義できる。

1
2
3
Rails.application.routes.draw do
  resources :articles, constraints: { id: /[0-9]{1,3}/ }
end

以下はコマンド bin/rails routes の実行結果である。articles#showなどでidに指定できる値に制限が加わっていることがわかる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ bin/rails
      Prefix Verb   URI Pattern                  Controller#Action
    articles GET    /articles(.:format)          articles#index
             POST   /articles(.:format)          articles#create
 new_article GET    /articles/new(.:format)      articles#new
edit_article GET    /articles/:id/edit(.:format) articles#edit {:id=>/[0-9]{1,3}/}
     article GET    /articles/:id(.:format)      articles#show {:id=>/[0-9]{1,3}/}
             PATCH  /articles/:id(.:format)      articles#update {:id=>/[0-9]{1,3}/}
             PUT    /articles/:id(.:format)      articles#update {:id=>/[0-9]{1,3}/}
             DELETE /articles/:id(.:format)      articles#destroy {:id=>/[0-9]{1,3}/}

アクション定義の幅

リソースを指定した場合、index, create, new, edit, show, update, destroyを自動で定義する。時によって、この定義は過剰な場合がある。

その場合はexceptもしくはonlyを使用する。以下はリソースarticlesに対してdestroyを除外するためにexceptを使用する例である。

1
2
3
Rails.application.routes.draw do
  resources :articles, except: ['destroy']
end

以下はコマンド bin/rails routes の実行結果である。destroyが存在しない。

1
2
3
4
5
6
7
8
9
$ bin/rails routes
      Prefix Verb  URI Pattern                  Controller#Action
    articles GET   /articles(.:format)          articles#index
             POST  /articles(.:format)          articles#create
 new_article GET   /articles/new(.:format)      articles#new
edit_article GET   /articles/:id/edit(.:format) articles#edit
     article GET   /articles/:id(.:format)      articles#show
             PATCH /articles/:id(.:format)      articles#update
             PUT   /articles/:id(.:format)      articles#update

コントローラーの指定

ルーティング設定でリソース名だけ指定した場合、自動で対応するコントローラーが決まる。以下の例ようのようにコントローラーの指定も可能である。

1
2
3
Rails.application.routes.draw do
  resources :articles, controller: 'goods'
end

以下はコマンド bin/rails routes の実行結果である。コントローラーがarticlesではなくgoodsになっていることがわかる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ bin/rails routes
      Prefix Verb   URI Pattern                  Controller#Action
    articles GET    /articles(.:format)          goods#index
             POST   /articles(.:format)          goods#create
 new_article GET    /articles/new(.:format)      goods#new
edit_article GET    /articles/:id/edit(.:format) goods#edit
     article GET    /articles/:id(.:format)      goods#show
             PATCH  /articles/:id(.:format)      goods#update
             PUT    /articles/:id(.:format)      goods#update
             DELETE /articles/:id(.:format)      goods#destroy

トップページの定義

トップページを config/routes.rb に定義できる。以下に例を示す。

1
2
3
4
5
6
7
Rails.application.routes.draw do
  resources :articles

  # <中略>
  
  root 'welcome#index' # 末尾に記載する
end

なお、Railsはルート定義よりも public/index.html を優先する点に留意すること。

TODO getとは?

TBD.

TODO リダイレクト

TBD.

コントローラー

MVCにおけるControllerについて取り扱う。

コントローラー作成

rails generate コマンドでコントローラーを作成する。同コマンドの詳細は rails generate controller --help で確認できる。以下にはコントローラー作成の一例を示す。

1
2
# rails generate controller NAME [action action] [options]
rails generate controller products

上述のコマンドは以下を作成する。

  • app/controllers/products_controller.rb
  • app/views/products … フォルダ
  • app/helpers/products_helper.rb
  • test/controllers/products_controller_test.rb

なお、コントローラー作成時にアクション(index, showなど)を指定できる。以下に例を示す。

1
rails generate controller Photos index create new edit show update destroy

前述のコマンドは以下のファイル、フォルダを作成する。

  • app/controllers/photos_controller.rb
  • app/views/photos
  • app/views/photos/index.html.erb
  • app/views/photos/create.html.erb
  • app/views/photos/new.html.erb
  • app/views/photos/edit.html.erb
  • app/views/photos/show.html.erb
  • app/views/photos/update.html.erb
  • app/views/photos/destroy.html.erb
  • test/controllers/photos_controller_test.rb
  • app/helpers/photos_helper.rb

また、アクションを指定した場合にはルーティング設定 (config/routes.rb) に変更が入る。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Rails.application.routes.draw do
  get 'photos/index'
  get 'photos/create'
  get 'photos/new'
  get 'photos/edit'
  get 'photos/show'
  get 'photos/update'
  get 'photos/destroy'
  resources :articles
end

モジュール名指定

1
rails generate controller admin::home index

実行結果例

rails generate controller admin::home index
      create  app/controllers/admin/home_controller.rb
       route  namespace :admin do
                get 'home/index'
              end
      invoke  erb
      create    app/views/admin/home
      create    app/views/admin/home/index.html.erb
      invoke  test_unit
      create    test/controllers/admin/home_controller_test.rb
      invoke  helper
      create    app/helpers/admin/home_helper.rb
      invoke    test_unit

この書き方も同様の結果となる。

1
rails generate controller admin/home index

実行結果例

rails generate controller admin/home index
      create  app/controllers/admin/home_controller.rb
       route  namespace :admin do
                get 'home/index'
              end
      invoke  erb
      create    app/views/admin/home
      create    app/views/admin/home/index.html.erb
      invoke  test_unit
      create    test/controllers/admin/home_controller_test.rb
      invoke  helper
      create    app/helpers/admin/home_helper.rb
      invoke    test_unit

アクション定義

アクションはコントローラーに記述する。アクションはパブリックメソッドであること。

1
2
3
4
5
6
class ArticlesController < ApplicationController
    # 以下のindexメソッドがアクションに該当する
    def index
        render plain: 'Hello!'
    end
end

リクエストヘッダ

リクエストヘッダの情報は request.headers から取得できる。

1
2
3
4
5
class ArticlesController < ApplicationController
    def index
        render plain: request.headers['user-agent']
    end
end
1
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15

HTTPステータスコード

RailsにはHTTPステータスコードに応じたシンボル定義がある。

https://railsguides.jp/layouts_and_rendering.html に詳細な表がある。

ステータスコード 用途 シンボル
200 正常応答 :ok
400 不正なリクエスト :bad_request
401 未認可 or 未承認 :unauthorized
403 アクセス禁止 :forbidden
404 リソースがない :not_found
500 サーバーの内部エラー :internal_server_error
503 サービス利用不可 :service_unavailable

renderメソッドはHTTPステータスコードを示す :status を持つ。以下に例を示す。

1
2
3
4
5
class ArticlesController < ApplicationController
    def index
        render status: 500
    end
end

ビューテンプレートを指定する

Railsはコントローラーとアクションの名称からレスポンスで使うビューテンプレートを選ぶためアプリ開発者は何もしないで済む。ただ、異なるアクションを指定したり、そもそもコントローラーも別のものを指定したい場合がある。

別のアクションを指定したい場合は以下のようにする。

1
2
3
4
5
class ArticlesController < ApplicationController
    def index
        render 'example'
    end
end

別のコントローラーを指定したい場合はアクションも併せて指定する。 template: を使う。以下に例を示す。

1
2
3
4
5
class ArticlesController < ApplicationController
    def index
        render template: 'photos/index'
    end
end

プレーンテキストのレスポンス

1
2
3
4
5
class ArticlesController < ApplicationController
    def index
        render plain: 'Hello!', status: :ok
    end
end

JSONレスポンス

1
2
3
4
5
6
class ArticlesController < ApplicationController
    def index
        @articles = Article.all
        render json: @articles
    end
end

render json: でJSON形式の応答をする。以下のJSONは整形したもの。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[
    {
        "id": 1,
        "title": "test1",
        "text": "test1",
        "created_at": "2023-12-03T10:06:44.593Z",
        "updated_at": "2023-12-03T10:21:29.108Z"
    },
    {
        "id": 2,
        "title": "test2",
        "text": "test2",
        "created_at": "2023-12-03T10:08:42.593Z",
        "updated_at": "2023-12-03T10:08:42.593Z"
    }
]

XMLレスポンス

XML応答をしたい場合はGemパッケージactivemodel-serializers-xmlを使う。GitHubにコードがある。

1
./bin/bundle add activemodel-serializers-xml

render xml: でXML形式の応答をする。

1
2
3
4
5
6
class ArticlesController < ApplicationController
    def index
        @articles = Article.all
        render xml: @articles
    end
end

以下はXML応答の結果である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<articles type="array">
    <article>
        <id type="integer">1</id>
        <title>test1</title>
        <text>test1</text>
        <created-at type="dateTime">2023-12-03T10:06:44Z</created-at>
        <updated-at type="dateTime">2023-12-03T10:21:29Z</updated-at>
    </article>
    <article>
        <id type="integer">2</id>
        <title>test2</title>
        <text>test2</text>
        <created-at type="dateTime">2023-12-03T10:08:42Z</created-at>
        <updated-at type="dateTime">2023-12-03T10:08:42Z</updated-at>
    </article>
</articles>

なお、Gemパッケージactivemodel-serializers-xmlがない状態では以下のようなXML応答になる。

TBD: XML例

TODO バイナリレスポンス

TBD.

  • send_file
  • send_data

応答可能な拡張子の定義

config/initializers/mime_types.rb に記述する。例えばUSDZファイルを返したい場合は次のようにする。

1
Mime::Type.register "model/vnd.usdz+zip", :usdz

コントローラーのアクションに respond_to で処理を分岐する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class ArticlesController < ApplicationController
    def index
        @articles = Article.all

        respond_to do |format| 
            format.html
            format.usdz { 
                send_file "#{Rails.root}/app/assets/images/sample.usdz"
            }            
        end
    end
end

ウェブブラウザでアクセスした際のURL(拡張子)で処理が分岐する。

なお、USDZ形式のファイルは https://developer.apple.com/augmented-reality/quick-look/ で3D modelsのところから入手できる。

ログ出力

Railsのコントローラーで以下のログレベルでログ出力ができる。

unknown(log)
不明なエラー
fatal(log)
致命的なエラー
error(log)
エラー
warn(log)
警告
info(log)
情報
debug(log)
デバッグ用情報
1
logger.info('エラー情報')

ログはRailsプロジェクトフォルダ直下のlogフォルダに保存する。

ログ削除

蓄積したログは以下のコマンドで削除する。

1
railes log:clear

TODO セッション

TBD.

TODO クッキー

TBD.

TODO ビュー

MVCにおけるViewについて取り扱う。

テンプレート変数 (名称要確認)

コントローラーのアクションメソッドからビューテンプレートに変数を介してデータを渡せる。

app/controllers/articles_controller.rb

1
2
3
4
5
class ArticlesController < ApplicationController
    def index
        @articles = Article.all
    end
end

app/views/articles/index.html.erb

1
2
3
4
5
<ul>
  <% @articles.each  do |article| %>
  <li><%= article.title %> / <%= article.text %></li>
  <% end  %>  
</ul>

出力結果のHTML

1
2
3
4
5
<ul>
  <li>タイトル1 / 本文1</li>
  <li>タイトル2 / 本文2</li>
  <li>タイトル3 / 本文3</li>
</ul>

ハイパーリンク

1
2
3
<%= link_to 'リンクタイトル', 'https://example.com' %>
<%= link_to 'Articles', {controller: 'articles', action: 'index'} %>
<%= link_to 'Article 1', Article.find(1) %>
1
2
3
<a href="https://example.com">リンクタイトル</a>
<a href="/articles">Articles</a>
<a href="/articles/1">Article 1</a>

TODO レイアウト

TBD.

TODO 部分テンプレート

部分テンプレートはファイル名をアンダースコア _ から始める。ファイルの格納先はRuby on Rails 7 ポケットリファレンスによると以下の通りである。

  • 特定のコントローラーの場合

    • app/views/{controller_name}
  • アプリケーション全体で共有する場合

    • app/views/application
    • app/views/shared

以下は例である。

app/controllers/articles_controller.rb

1
2
3
4
5
class ArticlesController < ApplicationController
    def index
        @articles = Article.all
    end
end

app/views/articles/index.html.erb

1
2
<h1>ページタイトル</h1>
<%= render 'article_list', articles: @articles %>

app/views/articles/_article_list.html.erb

1
2
3
4
5
<ul>
  <% @articles.each  do |article| %>
  <li><%= article.title %> / <%= article.text %></li>
  <% end  %>  
</ul>

出力結果のHTML

1
2
3
4
5
6
<h1>ページタイトル</h1>
<ul>
  <li>タイトル1 / 本文1</li>
  <li>タイトル2 / 本文2</li>
  <li>タイトル3 / 本文3</li>
</ul>

モデル

RailsはORMフレームワークActive Recordを提供する。本節はActive Recordの利用を前提におく。

モデルを定義する

rails generate model コマンドはモデルを自動生成する。

例えば、nameとemailを持つUserモデルを作成する場合は以下のコマンドを実行する。

1
rails generate model User name:string

nameの後ろにフィールドの型を指定している。 rails generate model --help の『Available field types:』では指定できる型として以下を挙げている。

  • integer
  • primary_key
  • decimal
  • float
  • boolean
  • binary
  • string
  • text
  • date
  • time
  • datetime

このコマンドを実行すると、Userクラスが定義される。 app/models/user.rb で確認できるだろう。 またマイグレーションと呼ばれる新しいファイルが生成される。

1
2
class User < ApplicationRecord
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
1
rails db:migrate

rails console コマンドで対話形式の画面に切り替わる。その画面でテーブル定義を確認できる。

1
rails console

以下のようにテーブル一覧やカラム一覧を確認する。

1
2
3
4
5
6
7
8
# テーブル一覧を表示する
irb(main):001> ActiveRecord::Base.connection.tables
=> ["schema_migrations", "ar_internal_metadata", "users"]

# テーブルのカラム一覧を表示する。
# Userモデルを指定するとusersテーブルを指定したことになる。
irb(main):002> User.column_names
=> ["id", "name", "email", "created_at", "updated_at"]

TODO モデルに依存関係を付与する

モデル作成・取得・削除

rails console でUserモデルを例にした操作になるが、アプリケーションのコードでも同じように User.all といったように使える。

create

レコード登録

1
2
3
irb(main):003> User.create(name:"田中太郎", email:"ttanaka@example.com")
irb(main):004> User.create(name:"佐藤俊雄", email:"tsato@example.com")
irb(main):005> User.create(name:"伊藤賢治", email:"kito@example.com")
all
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
irb(main):006> User.all
User Load (0.3ms)  SELECT "users".* FROM "users" /* loading for pp */ LIMIT ?  [["LIMIT", 11]]
=> 
[#<User:0x0000ffffa890a788
  id: 1,
  name: "田中太郎",
  email: "ttanaka@example.com",
  created_at: Sat, 06 Jan 2024 13:48:51.986203000 UTC +00:00,
  updated_at: Sat, 06 Jan 2024 13:48:51.986203000 UTC +00:00>,
 #<User:0x0000ffffa890a648
  id: 2,
  name: "佐藤俊雄",
  email: "tsato@example.com",
  created_at: Sat, 06 Jan 2024 13:49:09.575257000 UTC +00:00,
  updated_at: Sat, 06 Jan 2024 13:49:09.575257000 UTC +00:00>,
 #<User:0x0000ffffa890a508
  id: 3,
  name: "伊藤賢治",
  email: "kito@example.com",
  created_at: Sat, 06 Jan 2024 13:49:30.324797000 UTC +00:00,
  updated_at: Sat, 06 Jan 2024 13:49:30.324797000 UTC +00:00>]
find
1
2
3
4
5
6
7
8
9
irb(main):007> User.find(1)
  User Load (0.6ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> 
#<User:0x0000ffffa89daa00
 id: 1,
 name: "田中太郎",
 email: "ttanaka@example.com",
 created_at: Sat, 06 Jan 2024 13:48:51.986203000 UTC +00:00,
 updated_at: Sat, 06 Jan 2024 13:48:51.986203000 UTC +00:00>
find_by
1
2
3
4
5
6
7
8
9
irb(main):008> User.find_by(email:"kito@example.com")
  User Load (0.6ms)  SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "kito@example.com"], ["LIMIT", 1]]
=> 
#<User:0x0000ffffa8910020
 id: 3,
 name: "伊藤賢治",
 email: "kito@example.com",
 created_at: Sat, 06 Jan 2024 13:49:30.324797000 UTC +00:00,
 updated_at: Sat, 06 Jan 2024 13:49:30.324797000 UTC +00:00>
where
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
irb(main):011> User.where("email like?", "t%")
  User Load (0.8ms)  SELECT "users".* FROM "users" WHERE (email like't%') /* loading for pp */ LIMIT ?  [["LIMIT", 11]]
=> 
[#<User:0x0000ffffa8912460
  id: 1,
  name: "田中太郎",
  email: "ttanaka@example.com",
  created_at: Sat, 06 Jan 2024 13:48:51.986203000 UTC +00:00,
  updated_at: Sat, 06 Jan 2024 13:48:51.986203000 UTC +00:00>,
 #<User:0x0000ffffa8912320
  id: 2,
  name: "佐藤俊雄",
  email: "tsato@example.com",
  created_at: Sat, 06 Jan 2024 13:49:09.575257000 UTC +00:00,
  updated_at: Sat, 06 Jan 2024 13:49:09.575257000 UTC +00:00>]
update
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
irb(main):012> User.find(1).update(name:"田中大樹")
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction
  User Update (1.0ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "田中大樹"], ["updated_at", "2024-01-06 14:04:32.808267"], ["id", 1]]
  TRANSACTION (1.0ms)  commit transaction
=> true
irb(main):013> User.find(1)
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> 
#<User:0x0000ffffa89d9880
 id: 1,
 name: "田中大樹",
 email: "ttanaka@example.com",
 created_at: Sat, 06 Jan 2024 13:48:51.986203000 UTC +00:00,
 updated_at: Sat, 06 Jan 2024 14:04:32.808267000 UTC +00:00>
destroy
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
irb(main):014> User.find(1).destroy
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  TRANSACTION (0.1ms)  begin transaction
  User Destroy (0.4ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]
  TRANSACTION (0.6ms)  commit transaction
=> 
#<User:0x0000ffffa890dfc8
 id: 1,
 name: "田中大樹",
 email: "ttanaka@example.com",
 created_at: Sat, 06 Jan 2024 13:48:51.986203000 UTC +00:00,
 updated_at: Sat, 06 Jan 2024 14:04:32.808267000 UTC +00:00>

Active Support

blank?

blank? メソッドは真偽値を返す。

false, nil, 空配列、空のハッシュ、空文字列の場合に true を返す。それ以外に false を返す。

true.blank?  # => false
false.blank? # => true
nil.blank?   # => true
[].blank?    # => true
{}.blank?    # => true
"".blank?    # => true 
" ".blank?   # => true

2.1 blank?とpresent? - Active Support コア拡張機能 - Railsガイド

present?

present? メソッドは真偽値を返す。

true.present?  # => true
false.present? # => false
nil.present?   # => false
[].present?    # => false
{}.present?    # => false
"".present?    # => false
" ".present?   # => false

2.1 blank?とpresent? - Active Support コア拡張機能 - Railsガイド

try

2.5 try - Active Support コア拡張機能 - Railsガイド

def foo
  user = User.new('Tanaka')
  user.info = nil
  if user.info.try(:name)
    puts 'Hello Tanaka'
  else
    puts 'Hello World'
  end
end

foo # => 'Hello World'

Active Record

カラム定義

カラム追加

カラムを追加するにはadd_columnメソッドを使う。

以下はusersテーブルにemailカラムを追加する例である。

1
2
3
4
5
class AddEmailToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :email, :string
  end
end
NOT NULL制約

カラムにNOT NULL制約を付与する方法はカラム追加時、既存のカラムで異なる。

カラム追加時はadd_columnメソッドの null オプションを使う。以下に例を示す。

1
2
3
4
5
class AddEmailToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :email, :string, null: false
  end
end

既存のカラムに対してはchange_column_nullメソッドを使う。第3引数でNOT NULL制約を付与できる。以下に例を示す。

1
2
3
4
5
class ChangeEmailColumnNullToUsers < ActiveRecord::Migration[7.1]
  def change
    change_column_null(:users, :email, false) # falseでNOT NULL制約を有効にする
  end
end

NOT NULL制約を外したい場合はchange_column_nullメソッドの第3引数をtrueにする。

UNIQUE制約

カラムにUNIQUE制約を付与するにはadd_indexメソッドの unique オプションを使う。

以下はusersテーブルのemailカラムにUNIQUE制約を付与する例である。

1
2
3
4
5
6
class AddEmailToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :email, :string
    add_index :users, :email, unique: true
  end
end

カラムにUNIQUE制約を付与すると併せてインデックスも付く。

UNIQUE制約を外したい場合は remove_index メソッドでインデックスを外す。

FOREIGN KEY制約

カラムにFOREIGN KEY制約を付与するにはadd_foreign_keyメソッドを使う。

以下はarticlesテーブルにFOREIGN KEY制約を付与するためにusersテーブルを指定した例である。この場合、 articles.user_idusers.id が結び付く。

1
2
3
4
5
6
class AddUserIdToArticles < ActiveRecord::Migration[7.1]
  def change
    add_column :articles, :user_id, :bigint
    add_foreign_key :articles, :users
  end
end

カラムからFOREIGN KEY制約を外すにはremove_foreign_keyメソッドを使う。

以下はarticlesテーブルからFOREIGN KEY制約を外す例である。

1
2
3
4
5
class RemoveUserForeignKeyToArticles < ActiveRecord::Migration[7.1]
  def change
    remove_foreign_key :articles, :users
  end
end
インデックス

カラムにインデックスを付与するにはadd_indexメソッドを使う。

以下はusersテーブルのemailカラムにインデックスを付与する例である。

1
2
3
4
5
class AddEmailToUsers < ActiveRecord::Migration[7.1]
  def change
    add_index :users, :email
  end
end

カラムからインデックスを外すにはremove_indexメソッドを使う。

以下はusersテーブルのemailカラムからインデックスを外す例である。

1
2
3
4
5
class RemoveEmailIndexToUsers < ActiveRecord::Migration[7.1]
  def change
    remove_index :users, :email
  end
end
デフォルト値

カラムのデフォルト値はカラム追加時にadd_columnメソッドの default オプションで指定するかchange_column_defaultメソッドを使う。

例えば、usersテーブルのstatusカラムのデフォルト値が draft だったものを NULL にしたい場合は以下となる。

1
2
3
4
5
class RemoveStatusDefaultValueToUsers < ActiveRecord::Migration[7.1]
  def change
    change_column_default(:users, :status, from: 'draft', to: nil)
  end
end

from オプションと to オプションを使いマイグレーションに可逆性を持たせることができる。

CSSバンドル

Rails 7のリリースノートcssbundling-railsを導入したことに触れている。Ruby on Rails GuidesのThe Asset Pipeline 10.3 cssbundling-railsでは以下のように紹介している。

cssbundling-rails allows bundling and processing of your CSS using Tailwind CSS, Bootstrap, Bulma, PostCSS, or Dart Sass, then delivers the CSS via the asset pipeline.

Sass - dartsass-rails

SassのコンパイラにDart Sassがある。このDart SassをRailsでも利用できるようにしたGemパッケージがdartsass-railsである。

インストール方法

dartsass-railsの導入方法はRuby on Rails Guidesの10.4 dartsass-railsGitHubのrails/dartsass-railsを参照するようにと記載がある。そのGitHubのページはインストール方法として以下のコマンドを示している。

1
2
./bin/bundle add dartsass-rails
./bin/rails dartsass:install

同パッケージのインストーラーは app/assets/stylesheets/application.scss を作成する。このファイルはインストール直後からSassコンパイラの対象である。コンパイル処理はコマンド ./bin/dev で実行する。このコマンドはRailsプロジェクトフォルダ直下にある Procfile.dev (以下に例)に依存する。

1
2
web: bin/rails server -p 3000
css: bin/rails dartsass:watch

dartsass-railsはコンパイル結果を app/assets/builds/ に出力する。

設定ファイル

app/assets/stylesheets/application.scss 以外のSassを設ける場合、 config/initializers/dartsass.rb を作成して設定する。

1
2
3
4
5
# Sassファイルのパスを以下に記入する。パスの起点は app/assets/stylesheets/ である。
Rails.application.config.dartsass.builds = {
  "app/index.sass"  => "app.css",
  "site.scss"       => "site.css"
}

TODO RSpec

TBD.

Swagger

Ruby on RailsでSwaggerを使う場合、以下のGemパッケージが候補に出ると思われる。

rswag

インストール方法

インストール方法はREADMEのGetting Startedに記載があるが、補足の手順が必要だったため、以下に記録を残す。

Gemfile に以下を記述する。

1
2
3
4
5
6
7
gem 'rswag-api'
gem 'rswag-ui'

group :development, :test do
  gem 'rspec-rails'
  gem 'rswag-specs'
end

Gemパッケージをインストールする。

1
2
cd ./path/to/your/rails-project
bundle install

rails コマンドを実行する。

1
2
3
4
5
6
7
8
9
rails g rswag:api:install
rails g rswag:ui:install

# 前述のREADMEのGetting Startedでは、rails g rspec:installの手順が割愛されており、
# 次に控えているrails g rswag:specs:installコマンドでエラーを起こす。
# どうやらrspec:installが実行されていることが前提にあるようだ。
rails g rspec:install

RAILS_ENV=test rails g rswag:specs:install

サンプルとして spec/requests/blogs_spec.rb を作成する。READMEのGetting Startedの3にあるソースコードを作成した blogs_spec.rb にコピー&ペーストする。

rails rswag を実行して swagger.yml を生成する。同コマンドは app/swagger/v1/swagger.yaml を作成する。 なお、 rails rswagrails rswag:specs:swaggerize のエイリアスである。

1
RAILS_ENV=test rails rswag

http://127.0.0.1:3000/api-docs にアクセスする。 注: 筆者の場合、 rails server -p 3001 としている。

file:./images/rails/rswag_01.png

TODO API定義の仕方

TBD.

Lint

使い方

以下のコマンドでLintを実行できる

rubocop

Bundlerでrubocopをインストールした場合、以下のコマンドでも実行可能

bundle exec rubocop

1

RBSは考慮外とする。