20201215のRubyに関する記事は30件です。

Railsのルーティングをdrawを使ってまとめる

はじめに

gitlabのroutes.rbにて、draw :apiの様に記載されているものを見つけ、とても便利だと思ったのでまとめます。

draw

draw :apiの様に記載すると、config/routes/api.rbに記載したルーティングを読みに行ってくれます。

例えば、namespaceごとにそれぞれ別のファイルにルーティングを記載することで、わかりやすくすることができます。

デフォルトで用意されているものではなく、gitlabのソースコードで独自に用意されていたものなので、少し下準備が必要になります。

下準備

config/initializers配下にdraw_routes.rbファイルを作成し、下記の内容を記載します。

ほとんど、gitlabのソースコードと同じです。

module DrawRoute
  RoutesNotFound = Class.new(StandardError)

  def draw(routes_name)
    drawn_any = draw_route(routes_name)

    drawn_any || raise(RoutesNotFound, "Cannot find #{routes_name}")
  end

  def route_path(routes_name)
    Rails.root.join(routes_name)
  end

  def draw_route(routes_name)
    path = route_path("config/routes/#{routes_name}.rb")
    if File.exist?(path)
      instance_eval(File.read(path))
      true
    else
      false
    end
  end
end

ActionDispatch::Routing::Mapper.prepend DrawRoute

使用例

管理画面のroutesを切り出す

config/routes配下にadmin.rbを作成し、下記の様に記載

namespace :admin do
  resources :users
end

routes.rbに

Rails.application.routes.draw do
  draw :admin
end

と記載することで、下記の様にroutesが作成されます。

                          admin_users GET    /admin/users(.:format)                                                                   admin/users#index
                                      POST   /admin/users(.:format)                                                                   admin/users#create
                       new_admin_user GET    /admin/users/new(.:format)                                                               admin/users#new
                      edit_admin_user GET    /admin/users/:id/edit(.:format)                                                          admin/users#edit
                           admin_user GET    /admin/users/:id(.:format)                                                               admin/users#show
                                      PATCH  /admin/users/:id(.:format)                                                               admin/users#update
                                      PUT    /admin/users/:id(.:format)                                                               admin/users#update
                                      DELETE /admin/users/:id(.:format)                                                               admin/users#destroy

使用例2

大きくなるモデルのroutesを切り出す。

例えば、users_controller.rbusers/licences_controller.rb users/activates_controller.rbがある場合に、user単位でフォルダにまとめます。

config/routes/user.rb

resources :users do
  scope module: :users do
    resource :activates, only: %i[update destroy]
    resources :licences
  end
end

そして、routes.rbで読み込むことで

Rails.application.routes.draw do
  draw :user
end

下記の様なルーティングが作成されます。

                       user_activates PATCH  /users/:user_id/activates(.:format)                                                      users/activates#update
                                      PUT    /users/:user_id/activates(.:format)                                                      users/activates#update
                                      DELETE /users/:user_id/activates(.:format)                                                      users/activates#destroy
                             licences GET    /users/licences(.:format)                                                                users/licences#index
                                      POST   /users/licences(.:format)                                                                users/licences#create
                          new_licence GET    /users/licences/new(.:format)                                                            users/licences#new
                         edit_licence GET    /users/licences/:id/edit(.:format)                                                       users/licences#edit
                              licence GET    /users/licences/:id(.:format)                                                            users/licences#show
                                      PATCH  /users/licences/:id(.:format)                                                            users/licences#update
                                      PUT    /users/licences/:id(.:format)                                                            users/licences#update
                                      DELETE /users/licences/:id(.:format)                                                            users/licences#destroy
                                users GET    /users(.:format)                                                                         users#index

終わりに

アプリが大きくなるにつれ、routes.rbがカオスになることがあると思うので、drawを使って良い感じにファイルを分けることで、すっきりしそうです。

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

他言語の視点で見るRuby

はじめに

Ruby その2 Advent Calendar 2020 15日目の記事です!!

今年の秋頃に、Ruby Association試験を受けました。

その時の試験勉強中に、「Rubyだとこう書くのか〜〜」とか「Rubyだとこの意味になるのか〜〜」みたいなことがただあったので、本当に軽く紹介しておきます!!

(全然知らないことが多いので、多めにみてください。。。)

勉強した時の教材はこちらから。気になったは人はやってみてください!!
https://gist.github.com/sean2121/945035ef2341f0c39bf40762cd8531e0

begin ~ resucue

Rubyの例外処理と言いたら、これ。
beginブロック内で発生したエラーをresucueで拾い、エラーハンドリングをするものです。

begin
  1 / 0
rescue ZeroDivisionError => error
  p error
end

# => #<ZeroDivisionError: divided by 0>

他の言語では、try ~ catchが大半だと思います。

try {
  nonExistentFunction();
} catch (error) {
  console.error(error);
}

// > ReferenceError: nonExistentFunction is not defined

Rubyでのtryとcatch

try

Rubyにもtryはありますが、例外処理のメソッドではなく、オブジェクトに対してのメソッドです。
https://www.rubydoc.info/docs/rails/4.1.7/Object:try

Rubyでのtryは、&.ほぼ同意義です。(&.のことをnilガードと呼ぶことが多いです。)
「ほぼ」なので、少しだけ違いがあります。

&.は、オブジェクトがnilでない場合にメソッドを呼び出します。
try は、オブジェクトのメソッドを呼び出すことができるときにメソッドを呼び出します。

# &.
10&.to_s        # => "10"
10&.hoge        # => Error: undefined method `hoge' for 10:Fixnum (NoMethodError)
nil&.to_s       # nil

# try
10.try(:to_s)   # => "10"
10.try(:hoge)   # => nil
nil.try(:to_s)  # => nil

catch

Rubyでのcatchは、ブロック内でthrowが走った時にその引数と一致していれば、抜けることができるものです。
throw ~ catchでワンセットですね。

catch :error do
  puts "hoge"
  throw :error
  puts "fuga"
end
# => hoge

これはこれで、begin ~ rescueと書き方が似てはいますが、だいぶ違います。
詳しくはこちら。
https://qiita.com/harvath/items/e716179538bc1da30f88

例外処理よりかは、C言語のgotoに近いものですね。

*変数

C言語やGo言語を書いている人にとっては、*変数はかなり見覚えがあると思います。

みんな大好き、ポインタです!

tour_of_go.go
import "fmt"

func main() {
    i, j := 42, 2701

    p := &i         
    fmt.Println(*p) 
    *p = 21         
    fmt.Println(i)  

    p = &j         
    *p = *p / 37   
    fmt.Println(j) 
}

/*
42
21
73
*/

ポインタは、値のメモリアドレスを指します。
簡単に表すとこんな感じです。(例:go)

i := 42
p := &i

fmt.Println(p)  // 0xc000122020
fmt.Println(&p) // 0xc000124018
fmt.Println(*p) // 42

fmt.Println(i)  // 42
fmt.Println(&i) // 0xc000122020
fmt.Println(*i) // エラーになる
変数 アドレス値
i i = 42 &i == p = 0xc000122020
p *p == i = 42 &p = 0xc000124018

ポインタについてはこちら。
https://qiita.com/yz2cm/items/01908cdfe56c304f2a14

ただ、Rubyでの*変数ポインタではありません!!(初めて知った時、めっちゃポインタだと勘違いしていた。。。)

Rubyでは、変数を配列にするものです。つまり、to_aメソッドと同じ意味のものです。
メソッドのキーワード引数につけると、可変長引数として利用できます。

def foo (a, *b)
  p a, b
end

foo(1, 2, 3, 4)

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

**変数

ちなみに、Rubyでは**変数も書くことができます。これは、Hashを意味していることが多いです。

def variadic_keyword(**hash_args)
  p hash_args
end

variadic_keyword(april:"spring", july:"summer")

# => {:april=>"spring", :july=>"summer"}

終わりに

本当にざっくり簡単に軽くまとめました!!

これ以外でもいろいろあると思うので、その都度更新していきます。
(試験の結果は聞かないでください。。。。)

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

【Rails】ハッシュタグ機能を実装

概要

ハッシュタグ機能を実装する方法をまとめます。

参照

https://glodia.jp/blog/3936/
https://qiita.com/Naoki1126/items/4ea584a4c149d3ad5472

これら2つの記事の情報を組み合わせて実装しました。
ありがとうございます。

完成イメージ

今回は、写真投稿アプリを題材に、写真のキャプションにハッシュタグを導入する方法を説明します。
661b81756d7347895004cbab39aaf1732.png

写真詳細画面のキャプションにリンク付きハッシュタグが記載され、クリックするとそのハッシュタグのページが表示される。

Image from Gyazo

開発環境

  • macOS Catalina 10.15.7
  • ruby 2.6.5
  • Rails 6.0.3.4

実装の流れ

  • 各種モデルとマイグレーションファイルを準備
  • ハッシュタグ保存・更新アクションをモデルに追加
  • ルーティングを設定
  • ヘルパーメソッドを作成
  • コントローラーにhashtagアクションを作成
  • ビューの編集

今回のコード

コードは、必要な部分以外は省略して記載します。

1. 各種モデルとマイグレーションファイルを準備

hashtagモデルを作成

%rails g model hashtag

中間テーブルを作成

%rails g model photo_hashtag_relation

マイグレーションファイルを編集

テーブルのカラムを設定します。

create_hashtags.rb
class CreateHashtags < ActiveRecord::Migration[6.0]
  def change
    create_table :hashtags do |t|
      t.string :hashname
      t.timestamps
    end
    add_index :hashtags, :hashname, unique: true
  end
end
create_photo_hashtag_relations.rb
class CreatePhotoHashtagRelations < ActiveRecord::Migration[6.0]
  def change
    create_table :photo_hashtag_relations do |t|
      t.references :photo, index: true, foreign_key: true
      t.references :hashtag, index: true, foreign_key: true
      t.timestamps
    end
  end
end

モデルを編集

バリデーションとアソシエーションを設定します。

hashtag.rb
class Hashtag < ApplicationRecord
  validates :hashname, presence: true, length: { maximum:99}
  has_many :photo_hashtag_relations
  has_many :photos, through: :photo_hashtag_relations
end
photo_hashtag_relation.rb
class PhotoHashtagRelation < ApplicationRecord
  belongs_to :photo
  belongs_to :hashtag
  with_options presence: true do
    validates :photo_id
    validates :hashtag_id
  end
end

マイグレート

%rails db:migrate

2. ハッシュタグ保存・更新アクションをモデルに追加

photoモデルに以下のコードを追加します。
これで、写真投稿(create)、編集(update)時にハッシュタグがhashtagsテーブルに保存されます。

photo.rb
# 省略

  has_many :photo_hashtag_relations
  has_many :hashtags, through: :photo_hashtag_relations
.
.
# 省略
.
.
  #DBへのコミット直前に実施する
  after_create do
    photo = Photo.find_by(id: self.id)
    hashtags  = self.caption.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
    photo.hashtags = []
    hashtags.uniq.map do |hashtag|
      #ハッシュタグは先頭の'#'を外した上で保存
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      photo.hashtags << tag
    end
  end

  before_update do 
    photo = Photo.find_by(id: self.id)
    photo.hashtags.clear
    hashtags = self.caption.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/)
    hashtags.uniq.map do |hashtag|
      tag = Hashtag.find_or_create_by(hashname: hashtag.downcase.delete('#'))
      photo.hashtags << tag
    end
  end

end

ルーティングを設定

photosコントローラーにhashtagアクションを定義し、getで各ハッシュタグのページを表示させます。
URLは末尾にハッシュタグ名が来るようにしました。
:nameに入る値はこの後ヘルパーメソッドで設定します。

例えば、#カメというハッシュタグだとURLは
.../photo/hashtag/カメ
となります。

route.rb
get '/photo/hashtag/:name', to: "photos#hashtag"

ヘルパーメソッドを作成

photos_helper.rbに以下のコードを記載します。
photos_helper.rbファイルがない場合は自分で作成します。

helperについては、https://www.sejuku.net/blog/28563 などを参照

このヘルパーメソッド使用により、リンク付きのハッシュタグが入ったキャプションが作成されます。
コード内では、ハッシュタグ名が末尾に入ったURLが作成され、ハッシュタグクリック時のリンク先として設定されています。
ハッシュタグ名の前のURLには、先程route.rbに書いたURLを書き込みます。

photos_helper.rb
module PhotosHelper
  def render_with_hashtags(caption)
    caption.gsub(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/){|word| link_to word, "/photo/hashtag/#{word.delete("#")}"}.html_safe
  end 
end

コントローラーにhashtagアクションを作成

ハッシュタグに紐付いた写真を@photosに代入し、hashtagビューで使用し表示させます。

photos_controller.rb
  def hashtag
    @user = current_user
    @tag = Hashtag.find_by(hashname: params[:name])
    @photos = @tag.photos
  end

ビューの編集

先程作成したヘルパーメソッドrender_with_hashtagsを記載することにより、リンク付きハッシュタグが記載されたキャプションを写真詳細画面に表示させます。

show.html.erb
# 省略

<%= render_with_hashtags(@photo.caption) %>

ハッシュタグ画面では、hashtagアクションで設定した@photosを取り込み、ハッシュタグに紐づく写真を1枚ずつ表示させます。

hashtag.html.erb
<h2>ハッシュタグ #<%= @tag.hashname %></h2>
  <ul>
    <% @photos.each do |photo| %>
      <li>
        <%= link_to photo_path(photo.id) do %>
          <%= image_tag photo.image.variant(gravity: :center, resize:"640x640^", crop:"640x640+0+0"), if photo.image.attached? %>
        <% end %>
      </li>
    <% end %>
  </ul>

おわりに

以上が、今回行ったハッシュタグ機能実装の方法です。
意味が理解できていないコードが多々ありますが、追々理解していければと思います。

初学者なので間違いがありましたら、ご指摘いただきたいですm(_ _)m

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

第11回、RubyでClass化

Mac OS-11.0.1 ruby-2.6.6p146

Class

本記事は大学院前期過程の講義[マルチスケールシミュレーション特論]に関する記事です
Ruby で Class を扱っていきます。

Class化する前のhelloメソッド

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

def gets_name
  name = ARGV[0]
  if name == nil
    puts "What\'s your name? "
    name = gets.chomp
  end
  return name
end

hello gets_name

これに対して、Hello Classを定義して、RubyにおけるClassをみていきます

いきなりClass化

いきなりですが、Class化が完了したソースコードを拝見。

class Hello
  def initialize
    @name = gets_name
    puts_hello
  end

  def puts_hello
    puts "Hello #{@name}."
  end

  def gets_name
    name = ARGV[0] || 'world'
    return name.capitalize
  end
end

Hello.new

なるほど。簡単やん。ここで注目すべきポイントを下に記述します。

  • initialize
    • これはいわゆるコンストラクタ。
  • @name
    • これはClassのメンバー変数となる。(アットマークをつける)
  • capitalize
    • 大文字にしてくれる。英単語そのままやけど。
  • Hello.new
    • これでハローclassの宣言が可能となる。

継承とかオーバーライド

これに関してもコードをみて学習。

# 継承
class Hello < String
  def hello
    "Hello #{self}."
  end
end

# オーバーライド
class String
  def hello
    "Hello #{self}."
  end
end

なるほど。すごく単純。ここで改めて思うこと、Ruby ってめちゃめちゃ簡単やし単純やし学習コスト低いなー。
ちなみに呼び出しとか使用は以下の通り

require 'colorize'
# 継承呼び出し
greeter = Hello.new(ARGV[0])
puts greeter.hello.green

# オーバーライド呼び出し
puts ARGV[0].hello.green

Ruby簡単。Cで組込み開発してるけどしたくなくなる。以上。


  • source ~/Library/Mobile Documents/com~apple~CloudDocs/KG/class/M1/multi_scale_sim/grad_members_20f/members/ryomichi56/./qiita/no11.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ポートフォリオ開発まとめ

これまで、2週間かけて取り組んだポートフォリオの要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

①要件定義
https://qiita.com/by-miwa30/items/93c4fc92b85e034b75cd

②README
https://qiita.com/by-miwa30/items/d7e0395c8c4a13dcff6f

③アプリの起動
https://qiita.com/by-miwa30/items/fbdaee77efaea6b6df11

④ユーザー管理機能を実装
https://qiita.com/by-miwa30/items/4d868f447bfe7ce040f1

⑤ストロングパラメーター
https://qiita.com/by-miwa30/items/fc761332dd0de9ef5bc2

⑥新規登録
https://qiita.com/by-miwa30/items/65e175ee8c86e05c83ca

⑦アプリ投稿
https://qiita.com/by-miwa30/items/dbdcccd494b33c1e713d

⑧アプリの詳細ページ
https://qiita.com/by-miwa30/items/d01dd9a6ca68063affcd

⑨コメント機能
https://qiita.com/by-miwa30/items/a97cebfb6f4617b7a275

⑩ユーザー詳細ページ
https://qiita.com/by-miwa30/items/e78811610bfdeac8d735

⑪AWSのアカウント作成
https://qiita.com/by-miwa30/items/ca356f6c021eb14fcfef

⑫S3で保存先を用意する
https://qiita.com/by-miwa30/items/dc068e273d8e1d0c1764

⑬EC2の初期設定
https://qiita.com/by-miwa30/items/09c972145fea0e919046

⑭本番環境でデータベースを作成
https://qiita.com/by-miwa30/items/b1a73dbbde0f1bbcfa74

⑮EC2のRailsを起動しよう(手動デプロイ)
https://qiita.com/by-miwa30/items/dd455dcb73d03e19012e

⑯環境変数の設定
https://qiita.com/by-miwa30/items/924a653faef7a03eba36

⑰本番環境Railsの起動エラー(手動)
https://qiita.com/by-miwa30/items/d7a37a9deccbe76c047e

⑱Webサーバーの設定
https://qiita.com/by-miwa30/items/31251da3c26a8129aa60

⑲デプロイを自動化
https://qiita.com/by-miwa30/items/d4b7b1fa3601bf1185ba

⑳IPアドレスにアクセスエラー(自動デプロイ
https://qiita.com/by-miwa30/items/fbc620dfe6e71d4a417c

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

ポートフォリオまとめ

これまで、2週間かけて取り組んだポートフォリオの要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

①要件定義
https://qiita.com/by-miwa30/items/93c4fc92b85e034b75cd

②README
https://qiita.com/by-miwa30/items/d7e0395c8c4a13dcff6f

③アプリの起動
https://qiita.com/by-miwa30/items/fbdaee77efaea6b6df11

④ユーザー管理機能を実装
https://qiita.com/by-miwa30/items/4d868f447bfe7ce040f1

⑤ストロングパラメーター
https://qiita.com/by-miwa30/items/fc761332dd0de9ef5bc2

⑥新規登録
https://qiita.com/by-miwa30/items/65e175ee8c86e05c83ca

⑦アプリ投稿
https://qiita.com/by-miwa30/items/dbdcccd494b33c1e713d

⑧アプリの詳細ページ
https://qiita.com/by-miwa30/items/d01dd9a6ca68063affcd

⑨コメント機能
https://qiita.com/by-miwa30/items/a97cebfb6f4617b7a275

⑩ユーザー詳細ページ
https://qiita.com/by-miwa30/items/e78811610bfdeac8d735

⑪AWSのアカウント作成
https://qiita.com/by-miwa30/items/ca356f6c021eb14fcfef

⑫S3で保存先を用意する
https://qiita.com/by-miwa30/items/dc068e273d8e1d0c1764

⑬EC2の初期設定
https://qiita.com/by-miwa30/items/09c972145fea0e919046

⑭本番環境でデータベースを作成
https://qiita.com/by-miwa30/items/b1a73dbbde0f1bbcfa74

⑮EC2のRailsを起動しよう(手動デプロイ)
https://qiita.com/by-miwa30/items/dd455dcb73d03e19012e

⑯環境変数の設定
https://qiita.com/by-miwa30/items/924a653faef7a03eba36

⑰本番環境Railsの起動エラー(手動)
https://qiita.com/by-miwa30/items/d7a37a9deccbe76c047e

⑱Webサーバーの設定
https://qiita.com/by-miwa30/items/31251da3c26a8129aa60

⑲デプロイを自動化
https://qiita.com/by-miwa30/items/d4b7b1fa3601bf1185ba

⑳IPアドレスにアクセスエラー(自動デプロイ
https://qiita.com/by-miwa30/items/fbc620dfe6e71d4a417c

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

Railsのエラーメッセージの日本語化がうまくいかない

現象

 エラーメッセージを日本語化するため、カラム名の日本語訳を記述したファイルがうまく機能しませんでした。

解決方法

 記述のスペーシングを整えたらうまくいきました。
 誤字脱字以外にもスペーシングで機能しないことがあることを新たに学ぶことができました。

config/locales/models/ja.yml
  # 階層構造をスペーシングで表現しないとうまく機能しない
  ja:
    activerecord:
      models:
        event: イベント
      attributes:
        event:
          name: イベント名
          place: 開催場所
          content: イベント内容

エラーメッセージを日本語化する手順

「rails-i18n」というgemをインストール

Gemfile
gem 'rails-i18n'


bundle install

config/application.rbの記述を編集

config/application.rb
config.i18n.default_locale = :ja

これで日本語化はできました。
しかし、カラム名はまだ英語表記のため、
(例)emailを入力してください
のようなエラーメッセージになってしまいます。

カラム名を日本語に訳す記述をする

 config/locales/models/に「ja.yml」というファイルを作成します。
 作成したファイル内にカラム名に対応する日本語を記述します。

config/locales/models/ja.yml
  # 階層構造をスペーシングで表現しないとうまく機能しない
  ja:
    activerecord:
      models:
        event: イベント
      attributes:
        event:
          name: イベント名
          place: 開催場所
          content: イベント内容

参考文献

Railsのバリデーションエラーのメッセージの日本語化

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

Rails6プロジェクト(API)をDockerで開発(少しイメージの軽量化)

本記事は東京学芸大学 櫨山研究室 Advent Calendar 2020の15日目の記事になります.

はじめに

本記事はRails6が出たのでその環境をDocker化し,また少しイメージを軽くする方法です(RDBはMysql8.0を想定).
起動を確認するところまでを記事にしています.
railsのプロジェクトはローカルで作成したもの使用しています(私はローカルで以下のように作成しています).

console
$ rails new api --api -d mysql

フロントとバックエンドを分けて開発を行いたかったので,Rails6プロジェクトをapiモードで作成しています.
また本記事ではフロントエンド側の記述はしていません.
普段はRailsを使用していないので,メモとして残します?‍♂️

開発環境

  • MacOs:
  • Docker:
  • docker-compose:
  • ruby:2.6.3(rbenv で管理: 参考記事)
  • Rails 6
  • Mysql8.0

1. 準備

今回は既存のRails6プロジェクト(apiモード)をDockerしていきます.

1.1 ディレクトリ構成

今回はdocker-compose.ymlやDockerfileが階層的になっているため,makeで実行していきます.

1.2 ファイルの準備

Makefile

Makefile
.PHONY: build
build:
    docker-compose -f docker/docker-compose.yml build

.PHONY: start
start:
    docker-compose -f docker/docker-compose.yml up --remove-orphans

.PHONY: start.background
start.background:
    docker-compose -f docker/docker-compose.yml up -d --remove-orphans

.PHONY: stop
stop:
    docker-compose -f docker/docker-compose.yml down
  • buildでパッケージの取得
  • startでアプリケーションを実行
  • start.backgroundでバックグラウンドでアプリケーションを実行
  • stopでアプリケーションを停止

docker-compose.yml

docker/docker-compose.yml
version: "3"
services:
  api:
    container_name: api
    build:
      context: ../.
      dockerfile: ./docker/api/Dockerfile
    tty: true
    working_dir: /api
    volumes:
      - ../api:/api
    env_file: # dockerディレクトリ直下にapi.envファイルを作成し環境変数を設定する.
      - api.env
    ports:
      - "8000:3000" # フロントエンド側が3000番開放になるので変更
    depends_on:
      - db
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    networks:
      - sample-rails-docker-network

  db:
    container_name: db
    build:
      context: ../.
      dockerfile: ./docker/mysql/Dockerfile
    volumes:
      - ./mysql/db:/docker-entrypoint-initdb.d
    env_file: # dockerディレクトリ直下にmysql.envファイルを作成し環境変数を設定する.
      - mysql.env
    networks:
      - sample-rails-docker-network

networks: # 名前解決できるようにネットワークの設定
  sample-rails-docker-network:
    driver: bridge

Dockerfile:Mysql関連

docker/mysql/my.cnf
FROM mysql:8.0 # mysq;8.0を使用する

EXPOSE 3306
ADD docker/mysql/my.cnf /etc/mysql/conf.d/my.cnf

CMD ["mysqld"]

3306番ポートを開放し,設定ファイルを読み込みます.

docker/mysql/Dockerfile
[mysqld]
character-set-server=utf8
default_authentication_plugin=mysql_native_password

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8

mysqlのvolumeはdocker/mysql/dbに設定しているので,必要な場合はそちらに配置してください.

Dockerfile:Rails 6関連

docker/api/Dockerfile
FROM ruby:2.6.3-alpine as builder # ruby:2.6.3-alpineを使用する,bundle installするためにbuilderとして使用 
RUN apk update && \
  apk upgrade && \
  apk --no-cache add mysql-client mysql-dev tzdata build-base && \
  gem install bundler

WORKDIR /tmp
COPY api/Gemfile Gemfile
COPY api/Gemfile.lock Gemfile.lock
RUN bundle install

FROM ruby:2.6.3-alpine
ENV LANG ja_JP.UTF-8 
RUN apk --update add mysql-client mysql-dev tzdata nodejs && \
  gem install bundler

ENV APP_HOME /api
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY api $APP_HOME

今回使用するimageはruby:2.6.3-alpineですが,それぞれの環境で変更しても大丈夫です.
マルチステージングにし,bundle installにだけ必要なもの(build-base)を省くようにする.(apk delで消しても問題ない.)
今回はmysqlが動く最低限の環境になっています.

Rails関連

Gemfile

api/Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.3'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.3', '>= 6.0.3.2'

# database
gem 'mysql2'

# Use Puma as the app server
gem 'puma', '~> 4.1'

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'listen', '~> 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Gemfileは,mysql2rack-corsを設定しています.
rack-corsはCORSの設定です.

こちらもそれぞれの開発環境に合わせて変更してください(変更時にはDockerfileでapk add で加えるパッケージもも変更する必要があるので気をつけてください).

cors.rb

api/config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

CORSの設定は全てを許可しているので,それぞれで変更しましょう.

ルーティング(routes.rb)

api/config/routes.rb
Rails.application.routes.draw do
  namespace 'api' do
    namespace 'v1' do
      resources :hoges
    end
  end
end

APIなのでルーティングをVersion管理しやすくしておきます.

コントローラー(hoges_controller.rb)

api/app/controllers/api/v1/hoges_controller.rb
module Api
  module V1
    class HogesController < ApplicationController
      def index
        render json: { status: 'SUCCESS', message: 'Hello Rails!'}
      end
    end
  end
end

2. ビルドとアプリケーションの実行

2.1 rails new

今回は既存のプロジェクトを使用したので,省略します.(参考記事)

2.2 rake db:create

今回はDBの接続確認はしないので省略します.
参考までにapi直下で,以下のコマンドで環境を作成できます.

console
$ rails g model post title:string text:text
$ rails g controller posts
$ rails db:create
$ rake db:migrate

2-3. 依存関係の解決&imageの取得

console
$ make build

apk add [パッケージ]bundle installをしますので,少し時間がかかります.

2-4. アプリケーションの実行

console
$ make start

Railsとmysqlが起動します.

起動の確認

起動確認

スクリーンショット 2020-12-15 21.09.33.png

いつもの画面が出てくれば問題なく起動できています.

api化の確認

console
% curl http://localhost:8000/api/v1/hoges
{"status":"SUCCESS","message":"Hello Rails!"}%

問題なくできています!!

2.5 他のイメージと比較

サイズはプロジェクト事態が存在しての大きさになります.

イメージ サイズ
ruby:2.6.3 1.1GB
ruby:2.6.3-alpine 387MB
ruby:2.6.3-alpine(ビルドベース削除) 264MB

bundle install時に必要なbuild-baseを削除するだけでかなり削減されています!

おわりに

Rails6が出たので,そのAPIプロジェクトのDocker化,微量ながら軽量化をしました.
特に元イメージとalpineの差はとても大きいので,イメージサイズを減らすことができてよかったです.
これで好きなフロントエンドを使用してRailsのAPIを使用します.

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

【Ruby ~繰り返し処理~】勉強メモ4

Rubyの復習。
ほぼ自分の勉強メモです。
過度な期待はしないでください。

繰り返し処理

  • eachメソッド

eachメソッドを使うと、配列の要素を順番に取り出して処理を行うことが出来る。
使用方法は、「配列.each do |変数名|」と書き、「end」までの間に実行したい処理を書く。

書き方
配列.each do |変数名|
  # 処理
end

eachメソッドは、配列の要素の数だけ繰り返し処理を行う。
「| |」で囲まれた変数に配列の要素が1つずつ入っていき、その上でeachメソッドの中の処理が実行される。
eachメソッド内の変数名は、好きな名前をつけられますが、配列の変数名の単数形にすることが慣習上多い。

使用例
colors = ["赤色", "青色", "黄色"]
colors.each do |color|
  puts "色: #{color}"
end
出力結果
色: 赤色
色: 青色
色: 黄色
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyのネイティブ拡張をRustで作成する

昨日、Mac環境でRubyのネイティブ拡張を作る記事を投稿したが、WindowsとLinuxでも動く設定が確認できた。

構成

基本は前回と同じ。

まず変わったのは build.rs で、ライブラリ名もRubyのコンフィグからWin, Mac, Linux共通の値が取得できた。

build.rs
use std::process::Command;

fn rb_config(key: &str) -> String {
    let output = Command::new("ruby")
        .args(&["-e", &format!("print RbConfig::CONFIG['{}']", key)])
        .output()
        .expect("failed run ruby");

    return String::from_utf8(output.stdout).unwrap();
}

fn main() {
    println!("cargo:rustc-link-search={}", rb_config("libdir"));
    println!("cargo:rustc-link-lib={}", rb_config("RUBY_SO_NAME"));
}

次が Rakefile で、Windowsでも動かしやすいようにMakefileから変更した。
RustがサポートするWindows環境はmingwとmsvcがあるが、mingwを強制している。

Rakefile
require 'fileutils'
require 'json'

NAME = JSON.parse(`cargo metadata --format-version=1`).dig("packages", 0, "name")
TARGET_SO = "#{NAME}.#{RbConfig::CONFIG["DLEXT"]}"

desc "Delete artifacts"
task :clean do
  sh "cargo clean"
  FileUtils.rm_f(TARGET_SO)
end

desc "Create native extension"
task :build do
  env = {}
  case RUBY_PLATFORM
  when /mingw/
    env = {"RUSTUP_TOOLCHAIN" => "stable-#{RbConfig::CONFIG["host_cpu"]}-pc-windows-gnu"}
    cargo_out = "target/release/#{NAME}.dll"
  when /darwin/
    cargo_out = "target/release/lib#{NAME}.dylib"
  when /linux/
    cargo_out = "target/release/lib#{NAME}.so"
  else
    raise "Platform #{RUBY_PLATFORM} is not supported"
  end
  sh env, "cargo build --release"
  cp cargo_out, TARGET_SO, preserve: true, verbose: true
end

desc "Run Ruby script"
task :run => [:clean, :build] do
  ruby "run.rb"
end

他は変更なし

今後

gemとして公開するには gemspec 等が必要なので今後調べていくつもり。

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

【Ruby】メソッドの引数名を指定する。後ろにつくコロン(:)の意味

個人メモです。

メソッドの引数に後ろにコロンのつく変数が指定されている記述の意味について。

▼こういうの
def jadge(x:)

def jadge(x:)
  p "exist x" if x.present?
end

引数の後ろにつくコロンの意味

引数の中で後ろにつくコロンは、変数名を指定している。(キーワード引数と呼ぶ)

通常、引数は渡された順に、第一引数、第二引数、、、に代入されるが、後ろにコロンをつければ、その変数名と合った引数の値を渡すことができる。

特に扱う変数が複数の場合は変数名で指定できるので便利。

コロンで変数名を指定
#関数定義
def jadge(x:, y:)
  p "exist x" if x.present?
  p "exist y" if y.present?
end


#関数実行
jadge(y: false, x: true)
=> "exist x"



▼通常

def jadge(x, y)
  p "exist x" if x.present?
  p "exist y" if y.present?
end

y = false
x = true
jadge(y, x)
=> "exist y"

変数名ではなく、渡した順番で値が入る。

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

Herokuでデプロイをやり直す方法

<この記事について>
Herokuでデプロイした後に、本番環境に反映されていなくて再度デプロイをやり直す
ことで無事に解決できたので備忘録として投稿!

[環境]
・Ruby 2.6.5,
・Rails 6.0.0
・macOS

<状況>
railsでアプリを制作時に、本番環境での動きを確認するために、Herokuでデプロイを実施。
一度目は問題なく動作したが、2回目で不具合発生。ビューについて、ローカル環境と同様の
実装となっていないため、試行錯誤するも解決できず。メンターに質問したところ、
新たに1つの操作をすることで解決できました。

以下、全てターミナルで操作となります。

$ git commit --allow-empty -m "任意のコミットメッセージ"

→通常、差分がないとcommit/pushできないが、差分がなくても強制実行してくれるコマンド

あとは、通常のHerokuへのデプロイと同じ流れです。

$ git push heroku master
$ heroku run raills db:migrate

本番環境で確認して問題なければ完了。

<終わりに>
ローカル環境で問題ないのに本番環境で同じように動作しないと焦りやすいですが、
このようなコマンドで解決できることを知っているだけで精神的にも安定すると思います!

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

【Ruby】present/blankメソッドと後置if。

個人メモです。

if文とpresent?blank?メソッドを使うことで、変数に値がセットされているかどうかで処理を分岐できる。

rubyではifを後ろに書くことができる(後置ifという)。これを使うとシンプルな記述ができる。

present?とblank?

object.present?
objectに値が存在するとtrueを返す。

object.blank?
objectに値がない、または空だとtrueを返す。
値なしになるのは、nil、empty("")の場合。

値なしの場合
irb(main):333:0> x
=> nil

irb(main):333:0> x.present?
=> false

irb(main):334:0> x.blank?
=> true
値ありの場合
irb(main):335:0> test
=> "テスト"

irb(main):336:0> test.present?
=> true
irb(main):337:0> test.blank?
=> false

補足

blank?と似たメソッドでnil?やempty?もある。
検証するオブジェクトと出力の関係は以下。

メソッド nil empty ("")
blank? true true
nil? true false
empty false true
present? false false
値がemptyの場合
irb(main):340:0> z = ""
=> ""
irb(main):341:0> z.empty?
=> true
irb(main):342:0> z.nil?
=> false


後置if

処理の後ろに条件式をかける。従来の記述の改行やendを省略できる。

処理 if 条件式

後置if
def jadge(x)
  "exist" if x.present?
end
通常のif文
def jadge(x)
  if x.present?
    "exist"
  end
end


変数の値有無による条件分岐

blankやpresentを使うことで条件分岐ができる。
後置ifを使えばスッキリ記述できる。

def jadge(x)
  y = "No" if x.blank?
  y = "Exist" if x.present?
  p "回答は #{y} です"
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル(第6版) マイクロポストの投稿機能のバックグラウンド動作

概要

マイクロポストの投稿は以下の2stepで進む。

  1. ログインしてホーム画面を表示させる。ログイン状態のホーム画面にはマイクロポストの投稿フォームが設置されている。
  2. マイクロポストの内容を入力して送信ボタンを押すと、マイクロポストの内容がDBに保存される。

詳細

上記の各stepで実行されるバックグラウンドの動作は以下の通りである。

  1. ログイン時のバックグラウンド動作についてはこちらを参照。/static_pages/homeへのGETリクエストを送信するとStaticPagesコントローラのhomeアクションが実行される。このhomeアクションでは、ユーザーIDに紐付ける形で@micropostが新規作成される(マイクロポスト関連部分のみ抜粋)。homeアクションの最後で、対応したview(/static_pages/home.html.erb)が呼び出され、ホーム画面が表示される。このviewへはhomeアクションで作成された@micropostが渡されており、Railsはこの中身が空であることから、マイクロポストの新規作成であると判断している。
  2. ユーザーがマイクロポスト入力フォームに内容を入力して投稿ボタンを押すと、/micropostsへのPOSTリクエストが送信され、Micropostsコントローラのcreateアクションが実行される。このcreateアクション実行の直前には、現在ログイン中のユーザーであるかどうかが確認される。当のcreateアクションではparams[:micropost][:content]とparams[:micropost][:image]の値のみを許可して@micropostに格納し、DBへの保存を試みる。DBへの保存に成功すると、root_urlへのリダイレクトが行われ、ホーム画面が表示される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】if文の条件式で変数を代入する。

個人メモです。

if(x = test)が何をしているかについて。rubyでは変数の条件式で代入が使える。

実例

if x = test
  p x
end

=> nil

if x = testで、変数testの値を変数xに代入している。

もし代入元の変数testがfalseなら結果はfalseになる。

p x
「p」は出力。変数xを出力するとの意味。



変数テストに値が存在すれば、if文の結果はtrueになる。

test = "テスト"

if x = test
  p x
end

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

【Ruby】シンボルの変数と文字列の変数の違い。[:a]と['a']の違いについて。

個人メモです。

オブジェクトの値の取得方法で、obj[:a]obj['a']では対象となるデータの構造が異なる。

オブジェクトがシンボルで作成されているか文字列かによる。


シンボルを使ったオブジェクト

obj = {a:1, b:2}の場合、プロパティ名はシンボルとなる。
データ取得時の指定もシンボルで行う。

オブジェクト
irb(main):165:0> obj = {a:1, b:2}
=> {:a=>1, :b=>2}
データの取得
##OK
irb(main):166:0> obj[:a]
=> 1

##エラー
irb(main):167:0> obj['a']
=> nil

シンボル形式のオブジェクトのプロパティ名を' 'で指定するとエラーになる。


文字列を使ったオブジェクト

obj2 = {"a" => 1, "b" => 2}のように文字列で作成したオブジェクトのデータ取得は文字列で指定する。

##オブジェクト定義
irb(main):169:0> obj2 = {"a" => 1, "b" => 2}
=> {"a"=>1, "b"=>2}

##データ取得
irb(main):170:0> obj2['a']
=> 1
irb(main):171:0> obj2["a"]
=> 1


##エラー
irb(main):172:0> obj2[:a]
=> nil

シンボルを使うとエラーになる。

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

Everyday Rails でRSpecを学ぶ。サンプルサプリをbundle install するまで。

テストコードの重要性を認識して、everyday rails にてRSpecを学びはじめました。
bundle installできるまでに時間がかかったので
記事にします。

まず、git hub からgit clone してきます。
そしてbundle install.
できない。

nokogiriのエラー。
source ~/.bash_profileにて環境変数を変えることにより解決。

gem ffiがインストールできない。

Gemflie.lockを削除後、bundle installで解決!

やった!

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

roman numerals

お題

アラビア数字 (arabic numerals) を受け取って, ローマ数字 (roman numerals) を返す method を書け.
(https://qiita.com/daddygongon/items/2d0a73a51ddab2d9da1b にある課題)

今回書いた code

とりあえず解説より先に, 今回書いた code を貼っとく.

def to_roman (arabic_numerals)
  pair_arabic_roman = [[1,"I"],[4,"IV"],[5,"V"],[9,"IX"],[10,"X"],[40,"XL"],[50,"L"],
               [90,"XC"],[100,"C"],[400,"CD"],[500,"D"],[900,"CM"],[1000,"M"]]
  roman_numerals = ""

  pair_arabic_roman.reverse_each do |p_a, p_r|
    while arabic_numerals >= p_a do
      arabic_numerals   = arabic_numerals - p_a
      roman_numerals    = roman_numerals  + p_r
    end
  end

  return roman_numerals
end

if $PROGRAM_NAME == __FILE__
  arabic_numerals = ARGV[0].to_i
  print "#{arabic_numerals}\t : #{to_roman(arabic_numerals)}\n"
end

(Integer class に組み込んだ版はページ最後に貼ってる)

解説

まず, 作成する method 概要を整理する

  • 関数名 : 何でも良い (ここでは to_roman としておく)
  • 入力 : アラビア数字 (arabic numerals)
  • 出力 : ローマ数字 (roman numerals)

では, さっそく作っていく

1 に対して I を出力

思うがままに書いたらこうなる.

def to_roman (arabic_numerals)
  if arabic_numerals==1
    print "I\n"
  else
    print "hoge\n"
  end
end

if $PROGRAM_NAME == __FILE__
  arabic_numerals = ARGV[0].to_i
  print "#{arabic_numerals}\n"
  to_roman(arabic_numerals)
end

もちろん実行通る.
ちなみに 1 以外の場合に "hoge" を出力してるのは何となく.

2 に対して II, 4 に対して IV を出力

素直に書くとこうなった.

def to_roman (arabic_numerals)
  if arabic_numerals==1
    print "I\n"
  elsif arabic_numerals==2
    print "II\n"
  elsif arabic_numerals==4
    print "IV\n"
  else
    print "hoge\n"
  end
end

if $PROGRAM_NAME == __FILE__
  arabic_numerals = ARGV[0].to_i
  print "#{arabic_numerals}\n"
  to_roman(arabic_numerals)
end

もちろん実行通る.

5 に対して V を返す

elsif 書くのめんどくさくなったので, ちょっとループで書いてみた.
あと, 関数内で出力するのではなく, 返り値を返すようにした.

def to_roman (arabic_numerals)
  pair_arabic_roman = [[1,"I"],
               [2,"II"],
               [4,"IV"],
               [5,"V"]]

  pair_arabic_roman.each do |arabic, roman|
    if arabic_numerals==arabic
      return roman
    end
  end

  return "hoge"
end

if $PROGRAM_NAME == __FILE__
  arabic_numerals = ARGV[0].to_i
  print "#{arabic_numerals}\n"
  print "#{to_roman(arabic_numerals)}\n"
end

もちろん実行通る.

1 ~ 10 に対して ローマ数字を返す

対応表 pair_arabic_roman にアラビア数字とローマ数字の対応全てを書くのはめんどくさすぎる.
なので, こうした.

def to_roman (arabic_numerals)
  pair_arabic_roman = [[1,"I"],[2,"II"],[4,"IV"],[5,"V"],[6,"VI"],[9,"IX"],[10,"X"]]
  roman_numerals = ""

  pair_arabic_roman.reverse_each do |arabic, roman| # reverse each にした
    if arabic_numerals >= arabic
      arabic_numerals   = arabic_numerals - arabic
      roman_numerals    = roman_numerals  + roman
    end
  end

  return roman_numerals
end

if $PROGRAM_NAME == __FILE__
  arabic_numerals = ARGV[0].to_i
  print "#{arabic_numerals}\n"
  print "#{to_roman(arabic_numerals)}\n"
end

roman_numerals の初期値を "" にして, どんどん文字列を連結していく作戦.

  • 3 の場合 : 3 = 2 + 1 なので, """II" になり, その後 "III" になる.
  • 8 の場合 : 8 = 6 + 2 なので, """VI" になり, その後 "VIII" になる.

1 ~ 20 に対して

if のところを while に改良.

def to_roman (arabic_numerals)
  pair_arabic_roman = [[1,"I"],[2,"II"],[4,"IV"],[5,"V"],[6,"VI"],[9,"IX"],[10,"X"]]
  roman_numerals = ""

  pair_arabic_roman.reverse_each do |p_a, p_r|
    while arabic_numerals >= p_a do
      arabic_numerals   = arabic_numerals - p_a
      roman_numerals    = roman_numerals  + p_r
    end
  end

  return roman_numerals
end

if $PROGRAM_NAME == __FILE__
  arabic_numerals = Range.new(1,20)
  arabic_numerals.each do |i|
    print "#{i}\t : #{to_roman(i)}\n"
  end
end

ここまで来たらほぼ完成.

ちなみに Range は python でもよく見るあれ.

1 ~ 2000 に対して

対応表 pair_arabic_roman をある程度整理して, こうなった.

def to_roman (arabic_numerals)
  pair_arabic_roman = [[1,"I"],[4,"IV"],[5,"V"],[9,"IX"],[10,"X"],[40,"XL"],[50,"L"],
               [90,"XC"],[100,"C"],[400,"CD"],[500,"D"],[900,"CM"],[1000,"M"]]
  roman_numerals = ""

  pair_arabic_roman.reverse_each do |p_a, p_r|
    while arabic_numerals >= p_a do
      arabic_numerals   = arabic_numerals - p_a
      roman_numerals    = roman_numerals  + p_r
    end
  end

  return roman_numerals
end

if $PROGRAM_NAME == __FILE__
  arabic_numerals = [51,97,99,439,483,499,500,732,961,999,1000,1999,2000]
  arabic_numerals.each do |i|
    print "#{i}\t : #{to_roman(i)}\n"
  end
end

もちろん実行通った. やったぜ.
こんな感じで, 冒頭に載せた code が出来上がった.

発展課題

整数 Integer class を拡張して,

999.to_roman #=> CMXCIX

と返すようにする.

さっき作った method を素直に Integer class に突っ込んだ.

class Integer
  def to_roman
    arabic_numerals = self
    pair_arabic_roman = [[1,"I"],[4,"IV"],[5,"V"],[9,"IX"],[10,"X"],[40,"XL"],[50,"L"],
             [90,"XC"],[100,"C"],[400,"CD"],[500,"D"],[900,"CM"],[1000,"M"]]
    roman_numerals = ""

    pair_arabic_roman.reverse_each do |p_a, p_r|
      while arabic_numerals >= p_a do
    arabic_numerals = arabic_numerals - p_a
    roman_numerals  = roman_numerals  + p_r
      end
    end

    return roman_numerals
  end
end

if $PROGRAM_NAME == __FILE__
  a = ARGV[0].to_i
  print "#{a}\t : #{a.to_roman}\n"
end

もちろん期待通りの出力になってる.

参考資料

roman_numerals


  • source ~/Lecture/multiscale_simulation/grad_members_20f/members/gagagagazelle/docs/roman_numerals.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】演算子"::(コロンコロン)"【定数】

まえがき

Railsを使っていれば、誰でも以下の記述を見たことあると思います。

models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

ApplicationRecordActiveRecord::Baseを継承していますよね。

なんにも変わったことありませんね。

・・・

・・

って、え、え、ちょっと待って下さい。
なんですか、::って!?!?!?
怖い!!!

ということで、Ruby初学者である私が、::について調べたことを健忘録として残します。

結論

::は2つの特徴があります。

1.名前空間の絶対パスを指定する

module Hoge
  class Fuga
    def self.fuga
      p "fugaです"
    end
  end
end

Hoge::Fuga.fuga #=> "fugaです"
Fuga.fuga #エラー

#Fugaクラスを作成
class Fuga
  def self.fuga
    p "fugaです"
  end
end

Hoge::Fuga.fuga #=> "fugaです"
Fuga.fuga #=> "fugaです"

2.定数のスコープ演算子

クラスまたはモジュールで定義された定数をスコープ外部から参照するためには::を使います。

class Hoge
  FUGA = "FUGA" #定数FUGA

  p FUGA #=> "FUGA"
end

#スコープ外から定数FUGAを出力する。
p Hoge::FUGA #=> "FUGA"

また、Rubyではクラスやモジュールも定数として扱われます。
例えばHogeクラスを生成した時、Hogeクラスオブジェクトは、Hogeという名前の定数に代入されています。

結局、ActiveRecord::Baseの::は??

ActiveRecord::Baseの::ActiveRecordモジュール内のBaseクラスを指しています!!
ActiveRecordにネストされたBaseクラスなんですね〜。
簡単に書くと以下の通りです。

active_record/base.rb
module ActiveRecord

#省略

  class Base
  #省略
  end

#省略

end

ActiveRecord::Baseの正式な中身は以下の通りです。
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb

ActiveRecordの機能についてはこの方が深くまとめております。
Rails: ActiveRecord::Baseメソッドのまとめ

参考

Ruby公式リファレンス 変数と定数
https://docs.ruby-lang.org/ja/latest/doc/spec=2fvariables.html#const

Railsガイド Active Record の基礎
https://railsguides.jp/active_record_basics.html

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

RSpecまとめ 3.モデルスペック

前回の続き。

モデルに対するテスト。モデルのバリデーションやクラスメソッド、インスタンスメソッドをテストしていきます。

モデルスペックの構造

モデルスペックには、以下の3つのテストを最低限入れます。

1.有効な属性で初期化された場合は、モデルの状態が有効(valid)になっていること
2.バリデーションを失敗させるデータであれば、モデルの状態が有効になっていないこと
3.クラスメソッドとインスタンスメソッドが期待通りに動作すること

モデルスペック作成

まずはUserモデルのテストです。
前回、スペックファイルの自動生成を設定しましたが、今回は既存のモデルに対するテストなので、手動でスペックファイルを生成します。

$ rails g rspec:model user

このコマンドで、spec/models/user_spec.rbが生成されるので、このファイルにスペックを書いていきます。

spec/models/user_spec.rb
repuire 'rails_helper'  #ヘルパーの読み込み

RSpec.describe User, type: :model do  #ブロック内にUserモデルのスペックをまとめている

  #name, email, passwordがあれば有効な状態であること
  it 'is valid with a name, email, and password' do  #一つ一つのスペックはitで始まるブロック内に記述

    user = User.new(
      name:     "Zeisho",
      email:    "hoge@hoge.com",
      password: "hogehoge"
    )
    expect(user).to be_valid  #userが有効(valid)ならパスする
  end

end

RSpecでは、テストしたい値をexpect()に渡し、後ろに続くマッチャを呼び出すことでテストを行います。

今回の場合、userをbe_valid(有効か)というマッチャでテストしています。
また、(user)の後ろのtoは、テストする値がマッチャに適合すればsuccess、しなければfaildをテスト結果として返します。
toの反対の意味を持つto_notもよく使うので、覚えておくと良いでしょう。

バリデーションのテスト

正しい値を与えたときにモデルが有効になっているかテストできたので、今度はバリデーションを失敗させるデータを与えたときにモデルが無効な状態かテストしていきます。

spec/models/user_spec.rb
  #nameがなければ無効であること
  it 'is invalid without a name' do
    user = User.new(
      name:     nil
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end

今回のスペックでは、userのnameをnilにした状態でvalid?メソッドを使ってuserの有効性を検証し、最後にuser.errors[:name]に"can't be blank"という内容のものが含まれていればパスするという内容です。
nameがないことがバリデーションに失敗した原因であるかを知りたいので、発生したエラーの内容をテストしています。
user.valid?で有効性を検証しないとエラーメッセージが出ないので、注意しましょう。

この方法に沿って、他のバリデーションのスペックも書いていきましょう。

spec/models/user_spec.rb
  #emailが重複していれば無効であること
  it 'is invalid without a duplicate email address' do
    User.create(
      neme:     "zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user = User.new(
      name:     "Skywalker"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("has already been taken")
  end

createを使ってユーザーを保存し、その後で同じemailを持ったユーザーを生成するとemailの重複エラーが出るかという内容のスペックです。

この調子でUserモデルのスペックを完成させましょう。

完成したら、Projectモデルのスペックも作っていきます。

$ rails g rspec:model project
spec/models/project_spec.rb
require 'rails_helper'

RSpec.describe Project, type: :model do
  # ユーザー単位では重複したプロジェクト名を許可しないこと
  it "does not allow duplicate project names per user" do
  user = User.create(
    name:     "Zeisho",
    email:    "hoge@hoge.com",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  new_project = user.projects.build(
    name: "Test Project"
  )
  new_project.valid?
  expect(new_project.errors[:name]).to include("has already been taken")
  end

  # 二人のユーザーが同じ名前を使うことは許可すること
  it "allows two users to share a project name" do
  user = User.create(
    name:     "Zeisho",
    email:    "hoge@hoge.com",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  other_user = User.create(
    name:     "Skywalker",
    email:    "hogehoge@hoge.com",
    password: "hogehoge"
  )
  other_project = other_user.projects.build(
    name: "Test Project"
  )
  expect(other_project).to be_valid
  end
end

ProjectはUserに紐づいているので、今の書き方だとテストに必要なインスタンスを作るだけでコードが冗長化してしまいました。この辺りの問題は後で解決していきます。

クラスメソッド、インスタンスメソッドのテスト

このアプリには、Projectモデルに紐づいたNoteがあり、プロジェクトのメモとして文字列を格納できるようになっており、Noteモデルには検索機能を実装しています。

app/model/note.rb
scope :search, ->(term) {
  where("LOWER(message) LIKE ?", "%#{term.downcase}%")
}

今回はこのクラスメソッドとスコープをテストしていきます。

$ rails g rspec:model note
spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do
  #検索文字列に一致するメモを返すこと
  it "returns notes that match the search term" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("first")).to include(note1, note3)
    expect(Note.search("first")).to_not include(note2)
  end

  #検索結果が見つからなければ空のコレクションを返すこと
  it "returns an empty collection when no results are found" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("message")).to be_empty
  end
end

マッチャについてもっと詳しく

これまで、3つのマッチャ(be_valid, include, be_enpty)を使ってきましたが、RSpecが提供するマッチャをもっと知りたい場合、rspec-expectationsを参照すると良いでしょう。

スペックをDRYにする

discribe, context, before, afterを使ってスペックをDRYにしていきます。

discribe, context

スペック群を分類してブロック内にまとめることができます。
Noteモデルのスペックを例に見ていきましょう。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  #バリデーション用のスペック群

  #メッセージ検索機能のスペック群
  discribe "search message for a term" do
    #一致するデータが見つかるとき
    context "when a match is found" do
      #一致する場合のexample群
    end
    #一致するデータが見つからないとき
    context "when no match is found" do
      #一致しない場合のexample群
    end
  end

end

before, after

全てのテストで使用するテストデータを一箇所にまとめることができます。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  before do
    @user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    @project = user.projects.create(
      name: "Test Project"
    )
  end

  #バリデーション用のスペック群

  #メッセージ検索機能のスペック群

end

beforeには以下のオプションを設定できます。

before(:each)
describeまたはcontextブロック内の各(each)テストの前に実行

before(:all)
describeまたはcontextブロック内の全(all)テストの前に一回だけ実行

before(suite)
テストスイート全体の全ファイルを実行する前に実行

exampleの後に後片付けが必要であれば、afterを使うことができます。

テストはDRYにし過ぎない!

テストは開発・本番環境とは違って可読性を優先してDRYにしていくので、もしスペックファイルの内容を確認するのにエディタのスクロールや、複数のファイルの行き来を頻繁に行っているならば、DRY過ぎます。
必要に応じてコードを重複させることを検討したり、ファイルを行き来しなくても役割のわかる変数・メソッド名をつけるよう心がけましょう。

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

Everyday Railsまとめ 3章『モデルスペック』

前回の続き。

モデルに対するテスト。モデルのバリデーションやクラスメソッド、インスタンスメソッドをテストしていきます。

モデルスペックの構造

モデルスペックには、以下の3つのテストを最低限入れます。

1.有効な属性で初期化された場合は、モデルの状態が有効(valid)になっていること
2.バリデーションを失敗させるデータであれば、モデルの状態が有効になっていないこと
3.クラスメソッドとインスタンスメソッドが期待通りに動作すること

モデルスペック作成

まずはUserモデルのテストです。
前回、スペックファイルの自動生成を設定しましたが、今回は既存のモデルに対するテストなので、手動でスペックファイルを生成します。

$ rails g rspec:model user

このコマンドで、spec/models/user_spec.rbが生成されるので、このファイルにスペックを書いていきます。

spec/models/user_spec.rb
repuire 'rails_helper'  #ヘルパーの読み込み

RSpec.describe User, type: :model do  #ブロック内にUserモデルのスペックをまとめている

  #name, email, passwordがあれば有効な状態であること
  it 'is valid with a name, email, and password' do  #一つ一つのスペックはitで始まるブロック内に記述

    user = User.new(
      name:     "Zeisho",
      email:    "hoge@hoge.com",
      password: "hogehoge"
    )
    expect(user).to be_valid  #userが有効(valid)ならパスする
  end

end

RSpecでは、テストしたい値をexpect()に渡し、後ろに続くマッチャを呼び出すことでテストを行います。

今回の場合、userをbe_valid(有効か)というマッチャでテストしています。
また、(user)の後ろのtoは、テストする値がマッチャに適合すればsuccess、しなければfaildをテスト結果として返します。
toの反対の意味を持つto_notもよく使うので、覚えておくと良いでしょう。

バリデーションのテスト

正しい値を与えたときにモデルが有効になっているかテストできたので、今度はバリデーションを失敗させるデータを与えたときにモデルが無効な状態かテストしていきます。

spec/models/user_spec.rb
  #nameがなければ無効であること
  it 'is invalid without a name' do
    user = User.new(
      name:     nil
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end

今回のスペックでは、userのnameをnilにした状態でvalid?メソッドを使ってuserの有効性を検証し、最後にuser.errors[:name]に"can't be blank"という内容のものが含まれていればパスするという内容です。
nameがないことがバリデーションに失敗した原因であるかを知りたいので、発生したエラーの内容をテストしています。
user.valid?で有効性を検証しないとエラーメッセージが出ないので、注意しましょう。

この方法に沿って、他のバリデーションのスペックも書いていきましょう。

spec/models/user_spec.rb
  #emailが重複していれば無効であること
  it 'is invalid without a duplicate email address' do
    User.create(
      neme:     "zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user = User.new(
      name:     "Skywalker"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    user.valid?
    expect(user.errors[:name]).to include("has already been taken")
  end

createを使ってユーザーを保存し、その後で同じemailを持ったユーザーを生成するとemailの重複エラーが出るかという内容のスペックです。

この調子でUserモデルのスペックを完成させましょう。

完成したら、Projectモデルのスペックも作っていきます。

$ rails g rspec:model project
spec/models/project_spec.rb
require 'rails_helper'

RSpec.describe Project, type: :model do
  # ユーザー単位では重複したプロジェクト名を許可しないこと
  it "does not allow duplicate project names per user" do
  user = User.create(
    name:     "Zeisho",
    email:    "hoge@hoge.com",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  new_project = user.projects.build(
    name: "Test Project"
  )
  new_project.valid?
  expect(new_project.errors[:name]).to include("has already been taken")
  end

  # 二人のユーザーが同じ名前を使うことは許可すること
  it "allows two users to share a project name" do
  user = User.create(
    name:     "Zeisho",
    email:    "hoge@hoge.com",
    password: "hogehoge"
  )
  user.projects.create(
    name: "Test Project"
  )
  other_user = User.create(
    name:     "Skywalker",
    email:    "hogehoge@hoge.com",
    password: "hogehoge"
  )
  other_project = other_user.projects.build(
    name: "Test Project"
  )
  expect(other_project).to be_valid
  end
end

ProjectはUserに紐づいているので、今の書き方だとテストに必要なインスタンスを作るだけでコードが冗長化してしまいました。この辺りの問題は後で解決していきます。

クラスメソッド、インスタンスメソッドのテスト

このアプリには、Projectモデルに紐づいたNoteがあり、プロジェクトのメモとして文字列を格納できるようになっており、Noteモデルには検索機能を実装しています。

app/model/note.rb
scope :search, ->(term) {
  where("LOWER(message) LIKE ?", "%#{term.downcase}%")
}

今回はこのクラスメソッドとスコープをテストしていきます。

$ rails g rspec:model note
spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do
  #検索文字列に一致するメモを返すこと
  it "returns notes that match the search term" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("first")).to include(note1, note3)
    expect(Note.search("first")).to_not include(note2)
  end

  #検索結果が見つからなければ空のコレクションを返すこと
  it "returns an empty collection when no results are found" do
    user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    project = user.projects.create(
      name: "Test Project"
    )
    note1 = project.notes.create(
      message: "This is first note.",
      user:    user
    )
    note2 = project.notes.create(
      message: "This is second note.",
      user:    user
    )
    note3 = project.notes.create(
      message: "First, preheat the oven.",
      user:    user
    )
    expect(Note.search("message")).to be_empty
  end
end

マッチャについてもっと詳しく

これまで、3つのマッチャ(be_valid, include, be_enpty)を使ってきましたが、RSpecが提供するマッチャをもっと知りたい場合、rspec-expectationsを参照すると良いでしょう。

スペックをDRYにする

discribe, context, before, afterを使ってスペックをDRYにしていきます。

discribe, context

スペック群を分類してブロック内にまとめることができます。
Noteモデルのスペックを例に見ていきましょう。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  #バリデーション用のスペック群

  #メッセージ検索機能のスペック群
  discribe "search message for a term" do
    #一致するデータが見つかるとき
    context "when a match is found" do
      #一致する場合のexample群
    end
    #一致するデータが見つからないとき
    context "when no match is found" do
      #一致しない場合のexample群
    end
  end

end

before, after

全てのテストで使用するテストデータを一箇所にまとめることができます。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  before do
    @user = User.cerate(
      name:     "Zeisho"
      email:    "hoge@hoge.com"
      password: "hogehoge"
    )
    @project = user.projects.create(
      name: "Test Project"
    )
  end

  #バリデーション用のスペック群

  #メッセージ検索機能のスペック群

end

beforeには以下のオプションを設定できます。

before(:each)
describeまたはcontextブロック内の各(each)テストの前に実行

before(:all)
describeまたはcontextブロック内の全(all)テストの前に一回だけ実行

before(suite)
テストスイート全体の全ファイルを実行する前に実行

exampleの後に後片付けが必要であれば、afterを使うことができます。

テストはDRYにし過ぎない!

テストは開発・本番環境とは違って可読性を優先してDRYにしていくので、もしスペックファイルの内容を確認するのにエディタのスクロールや、複数のファイルの行き来を頻繁に行っているならば、DRY過ぎます。
必要に応じてコードを重複させることを検討したり、ファイルを行き来しなくても役割のわかる変数・メソッド名をつけるよう心がけましょう。

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

[Ruby] 配列の中から数字を探せ

一言

最近なぜか忙しい・・・。
仕事はそんなに多くないが・・・。
なぜか
契約切れるから引っ越し準備してたら事務所も引っ越して・・・。
なんてタイミング

ということで本題
標題的に何か、眼鏡をかけた赤と白のボーダーの服を着ている人を連想させますが・・・。

配列の中から任意の数字を探せ

今日も朝学習で前にもやった課題にチャレンジ(2回目)

array=[1,3,5,6,9,10,13,20,26,31]
※出力例:
検索したい数字を入力してください
5  5は配列の2番目に存在します
7  7は配列内に存在しません

こんな感じで出せと
ちょっと忘れててわからず解説見てしまいました。
真ん中のデータを取り出して、左右で大小の比較、それを繰り返し処理で範囲を絞っていって最終的にどの数字かを当てていくやり方。
所謂、二分探索(バイナリーサーチ)というやつ

で、恥ずかしながら昨日あげた記事にコメントいただいた内容で初めて知った言葉で「線形探索(リニアサーチ)」なる言葉をご教授いただき、このやり方でやってみようと思い投稿しました。

def binary_search(array, num)
  array.each_with_index do |n, index|
    if n == num
      return index
    end
  end
  return 0
end
array=[1,3,5,6,9,10,13,20,26,31]

puts "検索したい数字を入力してください"
num = gets.to_i

result = binary_search(array, num)

if result == 0
  puts "#{num}は配列内に存在しません"
else
  puts "#{num}は配列の#{result+1}番目に存在します "
end

これで同じような出力結果が出せました。
線形探索などの言葉は意識してなかったですが、いつもやってるような感じだったのでこっちの方が個人的にはやりやすかった。

めでたし

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

RSpecのテスト中にMySQL client is not connected

開発環境

macOS Catalina 10.15.7
Ruby on Rails 6.0.0
RSpec 4.0.1
pry rails 0.3.9
FactoryBot 6.1.0

エラー内容

console
Failure/Error: _query(sql, @query_options.merge(options))

ActiveRecord::StatementInvalid:
  Mysql2::Error: MySQL client is not connected

どうやらMySQLクライアントとの接続が確立できていないようだ。

定義を見る限り、client が初期化されているにも関わらず、network socket (file descriptor) が無効な状態だとこのエラーになるみたいですね。
Mysql2 の "MySQL client is not connected" について

検証

テスト結果
テストの実行結果を見ると、途中まではテストが成功しているため、ひとまずbinding.pryで処理を止めながらテスト内容を確認してみたところ、なぜかすべてのテストが成功した。

console
Finished in 16.16 seconds (files took 2.21 seconds to load)
15 examples, 0 failures

仮説

FactoryBotのインスタンス生成の記述を増やしたタイミングでエラーがエラーが発生しはじめたため、ここで負荷がかかって処理が止まった可能性があると考えた。

対処法1

インスタンスを生成するタイミングでsleepで処理を待機させることにした。

RSpec.describe OrderItem, type: :model do
  describe '購入情報の保存' do
    before do
      @user = FactoryBot.create(:user)
      @item = FactoryBot.create(:item)
      @order_item = FactoryBot.build(:order_item)
      sleep 0.1 # 0.1秒待機
    end
# 省略

結果

エラーを吐かずにテストが安定して成功するようになった。

対処法2

config/environments/test.rbに以下の記述をすることでも対処出来た。

config/environments/test.rb
Rails.application.configure do
  config.active_job.queue_adapter = :inline
# 省略
end

参考リンク

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

authenticateメゾットについて。

user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])

user.authenticate(password)で
合っていればユーザーの情報を出す。
間違っていれば、falseを返すメゾット。

主にログイン等に使われる。

if user && user.authenticate(params[:session][:password])

この文は&&の論理積(and)はnilとfalse以外は
Rubyではnilとfalse以外のすべてのオブジェクトは、真偽値ではtrueになるという性質を利用して、ログインできるかできないかをif文を使って実行している。

真偽値についての理解が浅かったため理解するのに時間がかかった。。。

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

Railsのform_withについて掘り下げる

軽く自己紹介

はじめましての方は初めまして。伊東と申します。
大卒後未経験でプログラマーになりまして、気づけば7,8年くらい働いています。
2020年3月からDMM WebCampのメンターをしています。

対象読者

  • Ruby on Railsで初めてMVCフレームワークを触った
  • 検証ツールであまり見ない
  • 初学者の方

前提

$ rails -v
Rails 5.2.4.3
$ ruby -v
ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-linux]
$

Rubyのバージョン、Rails5系だよってだけです。バージョン互換などは考慮しておりませんが、書く内容はそんなに影響するような話じゃないのでは??って思っています。 authenticity_tokenの箇所でデフォルトの挙動が違うぽいです...本題というわけではないので各バージョンで調べてくださいmm

伝えたいこと

検証ツール見ていきましょうって話です。
普段からinput nameとかの属性をちゃんと見てる人は読む必要がない記事になってます。
本当に基本的なことしか書かないので悪しからず

そもそもActionView::Helpersってなに??

よく使うのはform系ではないかな?と思います。

  • form_with
  • text_field_tag
  • text_area_tag
  • submit_tagなど

詳しくは以下リンクから見てみてください。
https://api.rubyonrails.org/classes/ActionView/Helpers.html

事前準備

細かい説明は割愛して作っていきます。

プロジェクト作成らへん

ちなみに行頭の$マークはrootユーザー#じゃないって意味です。

$ rails new without_action-view-helpers # プロジェクト作成
# 省略
$ cd without_action-view-helpers/
$ rails g controller books index new # コントローラー(アクション含)作成
# 省略
$ rails g model Book title:string price:integer description:text # モデル作成
# 省略
$ rails db:migrate # DB反映
# 省略
$

ルーティングの設定

config/routes.rb
Rails.application.routes.draw do
  resources :books, only: [:index, :new, :create, :show, :edit, :update]
end
ルーティングの確認
$ rails routes | grep book # grepは文字列検索しています
                    books GET   /books(.:format)                                                                         books#index
                          POST  /books(.:format)                                                                         books#create
                 new_book GET   /books/new(.:format)                                                                     books#new
                edit_book GET   /books/:id/edit(.:format)                                                                books#edit
                     book GET   /books/:id(.:format)                                                                     books#show
                          PATCH /books/:id(.:format)                                                                     books#update
                          PUT   /books/:id(.:format)                                                                     books#update
$

controller

めーっちゃシンプルな形。

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def index
    @books = Book.all
  end

  def new
    @book = Book.new
  end

  def create
    book = Book.new(book_params)
    book.save
    redirect_to books_path
  end

  def show
    @book = Book.find(params[:id])
  end

  def edit
    @book = Book.find(params[:id])
  end

  def update
    book = Book.find(params[:id])
    book.update(book_params)
    redirect_to books_path
  end

  private
  def book_params
    params.require(:book).permit(:title, :price, :description)
  end
end

view

app/views/books/new.html.erb
<h1>Books#new</h1>

<div>
  <%= render 'books/form', book: @book %>
</div>

部分テンプレート化しています。

app/views/books/_form.html.erb
<%= form_with model: book, local: true do |f| %>

  <div>
    <%= f.label :title %>
    <%= f.text_field :title %>
  </div>

  <div>
    <%= f.label :price %>
    <%= f.number_field :price %>
  </div>

  <div>
    <%= f.label :description %>
    <%= f.text_area :description %>
  </div>

  <div>
    <%= f.submit :submit %>
  </div>

<% end %>

ブラウザの検証ツールを使用して掘り下げて見ていきます

new

スクショだとこんな感じ
スクリーンショット 2020-12-10 20.26.02.png

まずformタグやinput>hiddenの部分から見ていきます
<form action="/books" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="✓">
  <input type="hidden" name="authenticity_token" value="cgX+LsxfzE7A7F5Tm8GAlF2KdaifSdqdrWd6cOgazlcbZx8OQ4c6LazJtdRYIdPjzmfmPUGHkuBoxSQhdSqiIA==">
</form>

action?accept-charset?method?hidden?指定していない項目がいくつもでてきました。

method

methodはsubmitされた時のhttpリクエストメソッドです。

action

actionはformタグ内のsubmitボタンがポチィされた時に実行されるURI(パス)を指定します。
/books で methodがpostになっているので、books_controllerのcreateアクションに飛ぶという仕組みです。

form_withでaction指定してなくない?

urlというパラメーターの指定が無いときはmodelからactionのpathを勝手に作ってくれています。モデル名とコントローラー名が一致している場合は 省略可能です。 model: book と書くだけでいいので簡単ですね。

ちなみにnamespaceを使用している場合は model: [:admin, book] としてあげれば、/admin/books というURIを作ってくれます。便利

hidden

name="utf8" については調べてないです!割愛します
name="authenticity_token" についてはCSRFの対策として自動発行されているものです。他のサイトから不正なリクエストを受けないように発行してるものです。
今回は5.2系で試しているのでデフォルトで有効になっているようですがそれ以外のバージョンであれば必要に応じて設定すれば有効になります。

inputタグ

入力項目はどのようになっているか
<div>
  <label for="book_title">Title</label>
  <input type="text" name="book[title]" id="book_title">
</div>

スクリーンショット 2020-12-13 16.21.27.png

inputタグを見てみると
<input type="text" name="book[title]" id="book_title"> となっています。
type="text"は単純にテキストを入力出来るという意味です。text以外にもあるのでこれ見ればわかると思います。 (詳しくはこれ: http://www.htmq.com/html5/input.shtml )
name="book[title]" これが多分一番大事になってくると思います。Rails的にはコントローラー側で params[:book][:title] で入力された値が取得出来るという意味になります。これを理解していれば、入力された値がDBに保存できない?みたいなことは少なくなると思います。

edit

スクリーンショット 2020-12-13 17.08.31.png

適宜省略してます
<form action="/books/1" accept-charset="UTF-8" method="post">
  <input type="hidden" name="_method" value="patch">
  <div>
    <label for="book_title">Title</label>
    <input type="text" value="たーいとる" name="book[title]" id="book_title">
  </div>

  <div>
    <input type="submit" name="commit" value="submit" data-disable-with="submit">
  </div>

</form>

formタグ, hiddenタグ

action部分は action="/books/1" になっています。model: bookで指定している変数bookが保存済みの場合URIを自動的にbook.idを設定してくれています。

hiddenタグで新規の時には無かったものが存在しています。 <input type="hidden" name="_method" value="patch"> これでhttpメソッドをpatchにしています。
なんでformタグで指定しないの??ってなると思うんですけど、多分ですがform>methodではpost/getしか指定できないからだと思います(間違ってたら指摘してもらえると :bow: )。 http://www.htmq.com/html5/form.shtml

inputタグ

<input type="text" value="たーいとる" name="book[title]" id="book_title">

value="たーいとる" という項目が増えています。これで入力部分の値を指定しています。それ以外は同じなので割愛します。

まとめ

HTMLも単純に画面に表示されたものを見るのではなく、どのようにHTMLが生成されているかを確認したり意識することで、HTMLであったりRailsの仕組みを少しずつ理解出来るようになっていくと思います。

あまり長い文章書くの得意じゃないんですが、役に立てると嬉しく思います。。。
またお声掛け頂いたメンターの方ありがとうございます。

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

gem install時の"can't find header files"エラーの解決

はじめに

EC2インスタンス上のマシンでbundle install実行時にcan't find header filesエラー。解決した方法をメモとして残す。

環境

  • EC2インスタンス
    • ubuntu 18.04LTS
  • ruby 2.5.1

bundle install時のエラー

$ bundle install --path .bundle
[DEPRECATED] The `--path` flag is deprecated because it relies on being remembered across bundler inv
ocations, which bundler will no longer do in future versions. Instead please use `bundle config set p
ath '.bundle'`, and stop using this flag
Fetching gem metadata from https://rubygems.org/.......
Using bundler 2.1.4
Using multipart-post 2.1.1
Using ruby2_keywords 0.0.2
Using faraday 1.1.0
Using faraday_middleware 1.0.0
Using gli 2.19.2
Using hashie 4.1.0
Using websocket-extensions 0.1.5
Fetching websocket-driver 0.7.3
Installing websocket-driver 0.7.3 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

current directory:
/home/ubuntu/slack_bots/.bundle/ruby/2.5.0/gems/websocket-driver-0.7.3/ext/websocket-driver
/usr/bin/ruby2.5 -r ./siteconf20201215-14622-s23dju.rb extconf.rb
mkmf.rb can't find header files for ruby at /usr/lib/ruby/include/ruby.h

extconf failed, exit code 1

Gem files will remain installed in
/home/ubuntu/slack_bots/.bundle/ruby/2.5.0/gems/websocket-driver-0.7.3 for inspection.
Results logged to
/home/ubuntu/slack_bots/.bundle/ruby/2.5.0/extensions/x86_64-linux/2.5.0/websocket-driver-0.7.3/gem_make.out

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

In Gemfile:
  slack-ruby-client was resolved to 0.15.1, which depends on
    websocket-driver

websocket-driver (0.7.3)というgemをインストール時にエラーになった模様。
メッセージに表示されているようにgem install websocket-driver -v '0.7.3' --source 'https://rubygems.org/'を単体で実行してみたところ、今度は以下のようなエラーが発生。

$ sudo gem install websocket-driver -v '0.7.3' --source 'https://rubygems.org/'
Building native extensions. This could take a while...
ERROR:  Error installing websocket-driver:
    ERROR: Failed to build gem native extension.

    current directory: /var/lib/gems/2.5.0/gems/websocket-driver-0.7.3/ext/websocket-driver
/usr/bin/ruby2.5 -r ./siteconf20201215-14722-9uc5to.rb extconf.rb
mkmf.rb can't find header files for ruby at /usr/lib/ruby/include/ruby.h

extconf failed, exit code 1

Gem files will remain installed in /var/lib/gems/2.5.0/gems/websocket-driver-0.7.3 for inspection.
Results logged to /var/lib/gems/2.5.0/extensions/x86_64-linux/2.5.0/websocket-driver-0.7.3/gem_make.out

メッセージmkmf.rb can't find header files for ruby at /usr/lib/ruby/include/ruby.hにあるようにruby用のヘッダファイルが見つからないのが原因のよう。
自分では解決策がわからなかったので素直にエラーメッセージでぐぐったら、同じような問題に遭遇している方がいらっしゃったので参考にさせてもらった。

解決方法

自身の使っているrubyのバージョンに合わせたruby-devをインストールすることで解決。
今回はruby2.5だったのでruby2.5-devをインストール。

$ sudo apt install ruby2.5-dev

その後bundle installを実行するとできた。

$ bundle install --path .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 '.bundle'`, and stop using this flag
Fetching gem metadata from https://rubygems.org/.......
Using bundler 2.1.4
Using multipart-post 2.1.1
Using ruby2_keywords 0.0.2
Using faraday 1.1.0
Using faraday_middleware 1.0.0
Using gli 2.19.2
Using hashie 4.1.0
Using websocket-extensions 0.1.5
Fetching websocket-driver 0.7.3
Installing websocket-driver 0.7.3 with native extensions
Fetching slack-ruby-client 0.15.1
Installing slack-ruby-client 0.15.1
Bundle complete! 1 Gemfile dependency, 10 gems now installed.
Bundled gems are installed into `./.bundle`

おわりに

gemのインストール時に表示されるwith native extensionsっていうのは、そのgemがc言語のライブラリに依存していて別途コンパイルが必要だということ。今回はwebsocket-driverをコンパイルするためのライブラリがマシンにインストールされていなかった。

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

未経験で大学を休学したエンジニアが2020年に学んだこと

CAMPFIRE Communityでエンジニアをしております。Matsuiです。

趣味はスポーツ観戦、旅行などです。
2020年はプロ野球、UEFAチャンピオンズリーグ、F1&F2が個人的に盛り上がりました。好きなドライバーはMax Verstappenです。(誰にも伝わらないと思いますが、Daniel Ricciardoとのコンビのシーンがすごく好きです。)

2020年は大学を休学し未経験からエンジニアに挑戦をした一年でした。このアドベントカレンダーを利用して2020年に私が経験したこと、学んだこと、意識したことをまとめます。

エンジニアにチャレンジする前(2020/1時点)のスペック

- プログラミング経験はほぼゼロ。(SQLのみ若干経験あり)
- コンピュータやインターネットには比較的馴染みがある。(作る側ではなく、利用する側として。)
- 国立大学文系学部に通っている。

やったこと

2020年1~2月

  • 初めて rails new をする。
  • Railsガイドのチュートリアル?をやる。

2~3月

  • CAMPFIREのコードを読むようになる。
  • 社内でデータ取得用のカラム追加とタスクに日時挿入の処理を追加する。

4~6月

  • サポートサービスのコード分離とサービス追加。
    • 密結合状態にあったコードをコントローラーレベルで切り分け、コードの分離を行いました。
    • またクラウドファンディングとコミュニティではサービスの性質が異なる部分もあるため、コミュニティ独自のサポートサービスを追加しました。

7~8月

9~11月

  • サポートサービスをDBで管理の上、社内の管理画面から追加、編集などをできるようにしました。
    • これまでは追加、編集のためにデプロイをエンジニアに依頼する必要がありましたが、エンジニアなしで追加、編集できるようにしました。

11~12月

  • 社内で使用する管理画面の機能改修、脱ライブラリ化。
    • CAMPFIRE全体で社内で使用する管理画面を脱ライブラリ化し、独自のシステムを構築し移行する流れがあったのでそれに合わせて、主にコミュニティに関する機能の移行を担当しました。(しています。現在も進行中です。)

意識したこと、学び

※個人の意見です

組織をどうしたいか、プロダクトをどうしたいかは一旦考えるのをやめた。

  • 組織のことを考えることも大事ですが、自分の実力が不足している状態だったのでとにかく自分のことに集中しました。自分のことに集中することが組織のためくらい割り切っていました。

一通り業務がこなせるようになるまでは、情報もシャットアウト。

  • 技術は進歩も早く常に新しい情報が流れてくるのでできるだけキャッチアップしたほうが良いですが、基礎が無いままキャッチアップをしても効率も低いままですし、アウトプットにつながらないのでまずは基礎固めに集中しました。(特に初期は。)

GitHubのPR,issue,Slackの議論やレビューなどはできる限り目を通す。

  • どこに学びが転がっているか分からないのでGitHubのPR,issue,Slackの議論やレビューなどは自分に関係ないことでも、意識して積極的に目を通すようにしていました。(そして実装の際は徹底的にパクり参考にしました。他人の思考や脳は外部ライブラリだと思っています。)

分からない、困ったは一定時間たったら詳しい人に質問するべき。

  • ありきたりですが、初期はこれが出来ず聞いたら迷惑なのでは、自分で突き詰めて考えた方が良いのではと思っていました。しかし、わからないことは一定時間(1時間など)以上悩んでも大体解決しないのでスパッと聞いたほうが良いと考えるようになりました。聞くことは迷惑でもないですし、迷惑だったとしても他のところで取り返せば良いくらいに開き直るようになりました。

さいごに

足りない部分、課題はありすぎて書ききれない、認識しているものが全てでないので省略します。

最後まで読んでいただきありがとうございます。

質問、ご意見、アドバイスなどあればコメントまでお願いします。

大学は卒業する予定です。おわり。

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

[Rails]resources, member, collection ルーティング/名前付きパス/URLの覚え方[初学者向け]

はじめに

初めまして。ながえもんと申します。
3ヶ月超かけて、ようやくRailsチュートリアルと言う山を登り切ったので、

  • resourcesで生成されるRESTfulな7つのルーティング
  • memberメソッド、collectionメソッドで生成される任意ルーティング
  • それらのアクション名、URL、名前付きパス

についてまとめました。

resourcesで生成されるパス/URLは非常に便利な一方で、
「あれ、名前付きパスはuser_path?users_path?」「URLは/users? /user? /:id?」と混乱する場面が多く、何となくモヤモヤしたままチュートリアルを一周してしまった方も多いはずです(…よね?)。

そんな初学者仲間の皆様の参考になれば幸いです。

[環境]
ruby 2.6.3
Rails 6.0.3.4

RESTfulな7つのルーティング(基礎)

リソース名は、(例によって)users リソースとします。

routes.rb
resources :users

このコードを実装すると生成される7つのルーティングは、
アクション順でみると

action名 HTTPreq URL 名前付きパス
users#index GET /users users_path
users#show GET /users/:id user_path
users#new GET /users/:id/new new_user_path
users#create POST /users users_path
users#edit GET /users/:id/edit edit_user_path
users#update PATCH/PUT /users/:id user_path
users#destroy DELETE /users/:id user_path

こうですね。
(※アクション名は コントローラ名#アクション名 と言う記法に倣っています)

7つのアクション名とその順番に関しては、…頑張って覚えましょう。
ちょっとした覚えるコツとしては、
indexとshowはペア。indexはuserの集合(全体)を、showはuserの個体を「表示する機能」としてまとめて覚えましょう。
newとcreateもペア。newテンプレートのフォームに入力した情報を基に、createアクションに繋げてuserインスタンスを生成する事が多いでしょう。
editとupdateもペア。editテンプレートのフォームに入力した情報を基に、updateアクションに繋げてuserインスタンスの情報を更新する事が多いでしょう。
最後にdestroy。これはペアがいない孤独なアクション君です。

URLと名前付きパスの関係

次に、URLや名前付きパスの2つの命名ルールを理解しましょう。

【ルール①】 /:id の有無で分類
まず前提として、URLは/users で始まります。ここは複数形で統一です。
その後ろに/:idが有るか無いかで分類します。

/:idなしの時→ 名前付きパスは「複数形」のusers_path
/:idありの時→ 名前付きパスは「単数形」のuser_path

【ルール②】 URLの後ろに付くオプションは、名前付きパスでは文頭に
ここで言うオプションとは、

/users/○○
/users/:id/○○

上の○○部分。

このパターンのURLの名前付きパスはルール①も踏まえて

オプション名_users_path または
オプション名_user_path となります。

このように、頭にオプション名がくると言うルールで命名されています。

以上の2つのルールが分かれば、
/users/:id/new の名前付きパスは new_user_path

同様に
/users/:id/edit の名前付きパスは edit_user_path

となる事が理解できますね。

(この法則は、後で出てくるmemberやcollectionで生成する任意ルーティングにも当てはまりますので、覚えておきましょう!)

名前付きパスから7つのルーティングを再整理

今度は、今覚えた名前付きパスごとにルーティングを再整理しましょう。

名前付きパス HTTPreq URL 反応するaction
users_path GET /users users#index
POST /users users#create
new_user_path GET /users/:id/new users#new
edit_user_path GET /users/:id/edit users#edit
user_path GET /users/:id users#show
PATCH/PUT /users/:id users#update
DELETE /users/:id users#destroy

こうなります。

indexとcreateの2つがusers_path。
これはuserの集合(コレクション)に対するアクションと覚えましょう。
index:コレクション全体を表示する
create:コレクション全体に、1個インスタンスを加える

あとの5つはuser_pathがベースになります。
newとeditは先ほど見たようにオプション付きの名前付きパスですね。

このように2つの方向から整理すると、だいぶ理解も深まって来たでしょうか。

ちなみに、$ rails routes を実行した際にはこちらの「名前付きパスごと」にルーティングが表示されるので、じっくり眺めてみると更に理解が深まるでしょう。

memberメソッドとcollectionメソッド

次に、usersリソースに任意のルーティングを加えるmemberメソッド、collectionメソッド
この2つのメソッドが生成するURLと名前付きパスについても見ていきましょう。

これらは、resources :users にブロックとして渡します(ネストします)。

routes.rb
resources :users do
  member do
    get :foo, :bar
  end
  collection do
    get :hogehoge
  end
end

各メソッドの特性:
memberメソッドは、「user_path」に対してアクションを追加。
collectionメソッドは、「users_path」に対してアクションを追加。

メソッドの書き方:
get :foo, :bar の行に注目して下さい。
「HTTPリクエストの型 :任意アクション名, :任意アクション名 」と言う書き方になります。
※アクション名は複数渡すことも可能です。

上の例では、users#foo, users#bar, users#hogehoge
と言う3つのアクション(メソッド)が生成されました。

各アクションに対応するルーティングは、

『アクション名=URLや名前付きパスのオプション』

と考えると分かりやすいです。
オプションですから、URLでは「後ろ」に、名前付きパスでは「文頭」に来るルールでしたね。

生成されたfooアクション、barアクション、hogehogeアクションを含めて
名前付きパスごとにルーティングを整理し直します。

今の皆様方であれば、なぜこのような名前付きパスになるのか。
なぜこのようなURLなのか、理解できるはずです。

名前付きパス HTTPreq URL 反応するaction
hogehoge_users_path GET /users/hogehoge users#hogehoge
foo_user_path GET /users/:id/foo users#foo
bar_user_path GET /users/:id/bar users#bar
users_path GET /users users#index
POST /users users#create
new_user_path GET /users/:id/new users#new
edit_user_path GET /users/:id/edit users#edit
user_path GET /users/:id users#show
PATCH/PUT /users/:id users#update
DELETE /users/:id users#destroy

以上となります。

最後までお付き合い頂きありがとうございました。
拙い内容だったかと思いますが、
本記事が少しでも名前付きパスで混乱していた方の助けになれば幸いです。

またよろしくお願いいたします:runner:

ながえもん

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

【Ruby】pryによるREPL駆動開発

Rubyの強力なREPLであるpryの基本的な使い方と、RubyのREPL駆動開発について記します。

REPL駆動開発とは、REPLを中心とする開発スタイルのことです。一般的な開発フローは、

  1. プロジェクトのソースコードを編集する
  2. 実行可能な段階になったら、開発環境にデプロイするなどして動作を確認する
  3. 上手くいかないところを直す
  4. 1.に戻る

ですが、2.の段階まで仕上げるのは結構時間がかかり、その間に間違いも犯しやすいです。一方、REPLを開発の中心に据えると、コードを手軽に実行でき、内部の状態も把握しやすいため、コーディングのフィードバックが非常に早く得られます。

pryのインストール

pryをインストールするには、以下のコマンドを実行します。

$ gem install pry pry-doc

MacやLinuxで、ルート権限でない場合は、以下のコマンドを実行します。

$ sudo gem intall pry pry-doc

pryは本体。pry-docがあると、Ruby組み込みのクラスやメソッドのドキュメントやソースコードを見ることができます。

インストールできたらpryを起動します。

$ pry
[1] pry(main)>

少し触ってみると、まずirbとは異なり、シンタックスハイライトや、Tabによる入力補完が効くことが分かります。

コードを書いてファイルに保存する

とりあえず、適当なコードを書いてみます。

[1] pry(main)> def fizzbuzz(num)
[1] pry(main)*   1.upto(num) do |n|
[1] pry(main)*     if n % 3 == 0
[1] pry(main)*       print "Fizz\n"
[1] pry(main)*     elsif n % 5 == 0  
[1] pry(main)*       print "Buzz\n"
[1] pry(main)*     elsif n % 15 == 0  
[1] pry(main)*       print "FizzBuzz\n"
[1] pry(main)*     else   
[1] pry(main)*       print n.to_s + "\n"
[1] pry(main)*     end  
[1] pry(main)*   end  
[1] pry(main)* end  
=> :fizzbuzz
[2] pry(main)> fizzbuzz(15)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
Fizz
=> 1
[3] pry(main)> 

3の倍数ならFizzに、5の倍数ならBuzzに、15の倍数ならFizzBuzzに変換して、1から15までの整数を出力するコードです。でも、何かおかしいですね。15がFizzBuzzではなく、Fizzになっています。15は3の倍数でもあるから、3つ目の条件n % 15 == 0よりも先に、1つ目の条件n % 3 == 0にマッチしてしまったのが原因です。

fizzbuzzメソッド を書き直さなければいけません。しかし、また13行もタイプするのは面倒です。editコマンドを使えば、指定したメソッドをエディタで編集できます。

[3] pry(main)>edit fizzbuzz

起動したエディタで、メソッドの内容を以下のように修正し、保存してエディタを閉じれば、pryのセッションに戻ります。

def fizzbuzz(num)
  1.upto(num) do |n|
    if n % 15 == 0
      print "FizzBuzz\n"
    elsif n % 3 == 0
      print "Fizz\n"
    elsif n % 5 == 0
      print "Buzz\n"
    else
      print n.to_s + "\n"
    end
  end
end

修正したfizzbuzzメソッドをREPLで実行してみます。

[4] pry(main)> fizzbuzz(15)
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
=> 1

今度は上手くいきました。

ちなみに、editコマンドで立ち上がるエディタはデフォルトでは、MacやLinuxではnano、Windowsではnotepadだと思いますが、pryの設定ファイルで変更できます。pryの設定ファイルは、~/.pryrc、エディタのプロパティは、Pry.editorです。したがって、たとえばエディタをvimに変更したければ、以下のようにします。

$ echo "Pry.editor = 'vim'" >> ~/.pryrc

'vim'の部分は、'emacs'でも'code'でも、PATHの通ったディレクトリにある任意のエディタを指定して下さい。

プログラムが完成したので、このメソッドはファイルに保存します。ファイルに保存するには、save-file {メソッド名} --to {ファイル名}を実行します。たとえば、fizzbuzzメソッドをカレントディレクトリのfizzbuzz.rbに保存したければ、次のようにします。

[5] pry(main)>save-file fizzbuzz --to './fizzbuzz.rb'

新たにpryを起動する時に、既存のファイルを読み込む場合は、-rオプションを使います。たとえば、fizzbuzz/rbを読み込んでpryを起動したければ、以下のようにします。

$ pry -r "./fizzbuzz.rb"

コードをデバッグする

続いて、もう少し複雑なプログラムを書いてみます。バブルソートを書くことにします。隣り合う要素を比較し、昇順になっていなければ順番を入れ替えるアルゴリズムです。

[6] pry(main)> def bubble_sort(nums)
[6] pry(main)*   right_end = nums.length - 1
[6] pry(main)*   while right_end > 0
[6] pry(main)*     (0..right_end).each do |i|
[6] pry(main)*       if nums[i] > nums[i + 1]
[6] pry(main)*         nums[i], nums[i + 1] = nums[i + 1], nums[i]
[6] pry(main)*       end  
[6] pry(main)*     end  
[6] pry(main)*     right_end -= 1
[6] pry(main)*   end  
[6] pry(main)*   return nums
[6] pry(main)* end  
=> :bubble_sort
[7] pry(main)> bubble_sort([3, 2, 1])
ArgumentError: comparison of Integer with nil failed
from (pry):5:in `>'
[8] pry(main)> 

エラーが出てしまいました。Integerとnilを比較することはできないと言われています。例外がスローされているのは5行目ですから、iが何れかの値の場合に、num[i]またはnum[i + 1]nilとなり、それを比較しようとしたのが原因です。エラーの原因を突き止め、修正します。エディタでbubble_sortを編集します。

[9] pry(main)>edit bubble_sort

エラーが発生するiの値を確かめるために、以下のコードを挿入します。

def bubble_sort(nums)
  right_end = nums.length - 1
  while right_end > 0
    (0..right_end).each do |i|
      binding.pry if nums[i].nil? || nums[i + 1].nil? # <= 挿入したコード
      if nums[i] > nums[i + 1]
        nums[i], nums[i + 1] = nums[i + 1], nums[i]
      end
    end
    right_end -= 1
  end
  return nums
end

binding.pryがRuby処理系に評価されると、そのコンテクストでpryが起動します。どういうことかというと、こういうことです。

[10] pry(main)> bubble_sort([3, 2, 1])

From: /XXXXXXXX/pry-redefined(0x3fdb8dc62bec#bubble_sort):5 Object#bubble_sort:

     1: def bubble_sort(nums)
     2:   right_end = nums.length - 1
     3:   while right_end > 0
     4:     (0..right_end).each do |i|
 =>  5:       binding.pry if nums[i].nil? || nums[i + 1].nil?
     6:       if nums[i] > nums[i + 1]
     7:         nums[i], nums[i + 1] = nums[i + 1], nums[i]
     8:       end
     9:     end
    10:     right_end -= 1
    11:   end
    12:   return nums
    13: end

[1] pry(main)> 

bubble_sortの5行目を実行した時点で時が止まっています。この状態では以下のように、このスコープ内の変数などをREPLで評価することができます。

[1] pry(main)> i
=> 2
[2] pry(main)> nums[i].nil?
=> false
[3] pry(main)> nums[i + 1].nil?
=> true
[4] pry(main)> nums.length
=> 3
[5] pry(main)> 

エラーが出る直前の変数を調べて判ったことは、iが2のとき、i + 1が配列の境界外を指しているため、nums[i + 1]nilになってしまったということです。つまり、numsi番目とi + 1番目を参照するのだから、「iが0からright_end」までではなく、「i + 1が1からright_endまで、すなわちiは0からright_end - 1まで」の範囲を動かなければいけないということです。したがって、そのように修正します。

コードを再実行するには、exitを実行します。

[5] pry(main)> exit

元のコンテクストに戻ってきたら、edit bubble_sortでコードを修正します。

def bubble_sort(nums)
  right_end = nums.length - 1
  while right_end > 0
    (0..(right_end - 1)).each do |i| # <= right_end を right_end - 1 に
      # binding.pry を削除
      if nums[i] > nums[i + 1]
        nums[i], nums[i + 1] = nums[i + 1], nums[i]
      end
    end
    right_end -= 1
  end
  return nums
end

編集内容を保存して、pryのセッションに戻り、修正を確認します。

[10] pry(main)> bubble_sort([3, 2, 1])
=> [1, 2, 3]
[11] pry(main)> bubble_sort([3, 2])
=> [2, 3]
[12] pry(main)> bubble_sort([3])
=> [3]
[13] pry(main)> bubble_sort([])
=> []
[14] pry(main)>

修正が確認できましたので、ソースコードを保存します。

[15] pry(main)>save-file bubble_sort --to './bubble_sort.rb'

上の例では、変数iの値を参照しただけでしたが、もちろん通常REPLで行うのと同じように、ローカル変数に値を再代入してからコードを再実行することもできます。

[1] pry(main)> def add_tax(price)
[1] pry(main)*   tax = 1.08  
[1] pry(main)*   binding.pry
[1] pry(main)*   return (price * tax).to_i
[1] pry(main)* end  
=> :add_tax
[2] pry(main)> add_tax(1000)

From: (pry):3 Object#add_tax:

    1: def add_tax(price)
    2:   tax = 1.08
 => 3:   binding.pry
    4:   return (price * tax).to_i
    5: end

[1] pry(main)> tax = 1.10
=> 1.1
[2] pry(main)> exit
=> 1100
[3] pry(main)> 

このように、pryを使うことでコードの検証や修正が非常に迅速に行えます。

コンテクストを移動する

コンテクストとは、今どのオブジェクトやメソッドの内部にいるのかという情報です。コンテクストを移動すると、そのオブジェクトのインスタンス変数やメソッドのローカル変数を、参照したり変更したりできます。

例として、FIFOのデータ構造を表すクラスQueueと、そのインスタンスを2つ作成します。

[16] pry(main)*   def initialize(initial_list)
[16] pry(main)*     @queue = initial_list
[16] pry(main)*   end  
[16] pry(main)*   
[16] pry(main)*   attr_reader :queue
[16] pry(main)*   
[16] pry(main)*   def enqueue(value)
[16] pry(main)*     @queue.push(value)
[16] pry(main)*   end  
[16] pry(main)*   
[16] pry(main)*   def dequeue()
[16] pry(main)*     @queue.shift
[16] pry(main)*   end  
[16] pry(main)* end  
=> :dequeue
[17] pry(main)> q1 = Queue.new([])
=> #<Thread::Queue:0x00007fa61bf0b4f0 @queue=[]>
[18] pry(main)> q2 = Queue.new([])
=> #<Thread::Queue:0x00007fa617f08240 @queue=[]>
[19] pry(main)> q1.enqueue(1)
=> [1]
[20] pry(main)> q2.enqueue(2)
=> [2]
[21] pry(main)> q2.enqueue(3)
=> [2, 3]
[22] pry(main)>

コンテクストを移動するには、cd {移動先のオブジェクト}を実行します。たとえば、先程作成したq1に移動するには、以下のようにします。

[22] pry(main)> cd q1
[23] pry(#<Thread::Queue>):1>

lsで、現在のコンテクストで参照可能なオブジェクトを一覧できます。

[23] pry(#<Thread::Queue>):1> ls
Thread::Queue#methods: <<  clear  close  closed?  deq  dequeue  empty?  enq  enqueue  initial_list  length  marshal_dump  num_waiting  pop  push  shift  size
self.methods: __pry__
instance variables: @queue
locals: _  __  _dir_  _ex_  _file_  _in_  _out_  pry_instance
[24] pry(#<Thread::Queue>):1> @queue
=> [1]
[25] pry(#<Thread::Queue>):1>

現在のコンテクストから抜けて、元のコンテクストに戻るには、exitを実行します。

[25] pry(#<Thread::Queue>):1> exit
=> #<Thread::Queue:0x00007fa72c80ca48 @queue=[1]>

同様に、q2のコンテクストも覗いてみると、インスタンス変数@queueの値が異なることが確認できます。

[26] pry(main)> cd q2
[27] pry(#<Thread::Queue>):1> ls
Thread::Queue#methods: <<  clear  close  closed?  deq  dequeue  empty?  enq  enqueue  initial_list  length  marshal_dump  num_waiting  pop  push  shift  size
self.methods: __pry__
instance variables: @queue
locals: _  __  _dir_  _ex_  _file_  _in_  _out_  pry_instance
[28] pry(#<Thread::Queue>):1> @queue
=> [2, 3]
[29] pry(#<Thread::Queue>):1> exit
=> #<Thread::Queue:0x00007fa72429c4d0 @queue=[2, 3]>
[30] pry(#<Thread::Queue>):1> 

また、前セクションに書いたように、binding.pryを用いれば、実行中のメソッドのローカル変数を確認することもできます。

ドキュメントを読み書きする

作成したクラスにはドキュメントを付けておきます。ドキュメントは、所定の形式で記せばpryから閲覧することができますし、他のコマンドラインツールでHTMLなどに自動で変換することもできます。

まずは、先程作成したQueueクラスをファイルに保存します。カレントディレクトリのqueue.rbに保存するには、以下のようにします。

[30] pry(main)> save-file Queue --to './queue.rb'
queue.rb successfully saved
[31] pry(main)>

edit {ファイル名}で、保存したソースコードを編集できます。今回は、RubyのメジャーなドキュメンテーションスタイルであるYARDに従ってドキュメントを書きます。

[31] pry(main)> edit './queue.rb'
queue.rb
# FIFOのデータ構造
class Queue
  def initialize(initial_list)
    @queue = initial_list
  end

  # @return [Array] 現在のキュー
  attr_reader :queue

  # キューに値を格納する
  # @param value [*object] キューに格納するオブジェクト。
  # @return [Array] 値を格納した後のキュー
  def enqueue(value)
    @queue.push(value)
  end

  # キューから値を取り出す
  # @return [object | nil] 最も古い要素。要素が一つもない場合はnil。
  def dequeue()
    @queue.shift
  end
end

保存してエディタを閉じると、pryのセッションに戻り、編集後のコードが自動で読み込まれます。

ドキュメントを閲覧するには、show-source {クラス/メソッド名} -dを実行します。show-sourceには$というエイリアスもあります。

[32] pry(main)> $ Queue -d

From: queue.rb:1
Class name: Thread::Queue
Number of lines: 23

FIFOのデータ構造

# ソースコード。長いので略。
[33] pry(main)>
[33] pry(main)> $ Queue#queue -d

From: queue.rb:7:
Owner: Thread::Queue
Visibility: public
Signature: queue()
Number of lines: 3

return [Array] 現在のキュー

attr_reader :queue
[34] pry(main)>
[34] pry(main)> $ Queue#enqueue -d

From: queue.rb:10:
Owner: Thread::Queue
Visibility: public
Signature: enqueue(value)
Number of lines: 7

キューに値を格納する
param value [*object] キューに格納するオブジェクト。
return [Array] 値を格納した後のキュー

def enqueue(value)
  @queue.push(value)
end
[35] pry(main)> 
[35] pry(main)> $ Queue#dequeue -d

From: queue.rb:17:
Owner: Thread::Queue
Visibility: public
Signature: dequeue()
Number of lines: 6

キューから値を取り出す
return [object | nil] 最も古い要素。要素が一つもない場合はnil。

def dequeue()
  @queue.shift
end
[36] pry(main)>

その他

binding.pryで停止した後にステップ実行するにはpry-byebugが、pryをRailsで使うにはpry-railsがあります。

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

【個人アプリ作成】情報管理アプリ制作14日目

また、時間が空いてしまいました。。継続性の難しさを痛感します。
今日までやってきたことをまとめたいと思います。

実装できてきたこと

・マイページのレイアウト修正
・顧客データの要件定義
・顧客データ詳細画面
・ログイン画面の修正
・情報の取得
・プルダウンの実装

マイページ

スクリーンショット 2020-12-15 11.45.03.png

少しヘッダーを整理してレイアウトをすっきりさせました。
今後はヘッダーのユーザーのところにJSで装飾を施したり、検索機能の実装を行う予定です。

今のところ、顧客情報は社名だけといった感じですが、かなり寂しいので「何の情報を開示できると有益なのか」をもう少し要件定義した方が良さそうです。
実装はruby on railsで、Seeds.rbにデータを入れて取得させています。

顧客データ詳細画面

スクリーンショット 2020-12-15 11.42.25.png

基本情報の部分は一通り必要な情報を印字させて、先方関係者やグループ会社、トラブル・クレームは履歴データで持たせたいので、テーブルを分けアソシエーションで対応したいと思います。

まだ、顧客データの登録機能や、情報編集機能には着手できていないのでこれから実装していきたいと思います。

これまでやってきた感想

かなり進捗が悪く、原因を追求してみました。

・本業の仕事がかなり忙しい(いい意味で)。
→夜遅くなるのが日常茶飯事なので時間調整をして20時や21時にはこちらに着手できるようにする
・ある意味、色々やりすぎていた。特に実績もないのにPFを頑張って作ろうとしていたのはまずかった。今はアプリ開発とQiita、Githubに力を入れる。協業は随時進める。
・プライベートの慌ただしさ。
こればっかりは結婚だの同棲だの異動だので不可避なのでありきで考える。これまで考えられていなかった。

想定される工数の見積もりが甘いのとあらためて仕事は影響の出ない範囲で調整をかけていくようにしたい。

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