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

RailsでWixやShopifyのような独自ドメインの紐付けができるマルチテナントサービスを作る

背景

最近流行りのノーコードサービスでは自分のウェブサイトやサービスを作って、独自ドメインを登録できたりしますよね? あれどうやってやるんだろうな?Railsでもできるのかな?と思ったので挑戦してみました。

※この記事ではアプリケーションレベルでの、ドメイン、サブドメインの取り扱い、データの分離について紹介します。本番環境では場合によってプロキシーサーバーを立てたり、追加でのDNS設定が必要です。

独自ドメインの紐付けとは?

通常のウェブサービスだとユーザーのページは「https://サービスのドメイン.com/user/ユーザーのID」のような感じです。しかしWix,Shopify、BASEのようなノーコードサービスでは自分のドメイン、またはサブドメインを登録してサイトを展開することができます。独自ドメインの紐付けとは「https://サービスのドメイン.com/user/ユーザーのID」を「https://ユーザーが取得したドメイン.com」からアクセスできるようにすることです。

マルチテナントサービスとは?

プラットフォーム上にユーザーが独自のデータ領域を持つサービスです。例えば、SmartHRのようなSaaSサービスはクライアントが法人で法人は様々な社員データをサービスに登録することになります。この時、会社Aの個人情報が会社Bの個人情報と同じデータプールで管理されていたらリスクですよね? なのでこのようなサービスではデータ領域をユーザー(テナント)ごとに分けて管理することが多いです(マルチテナンシー)。

作るもの

オンラインで簡単にオンラインスクールを開設できるサービスを作ろう!(仮 anyclass)
講師(instructor)はスクール(school)を作成できる
schoolテーブルはdomain,subdomainカラムを持っていて自分で設定できる
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3232343937342f64376562376435662d343561302d343562612d363335352d6132313663366331623061332e706e67.png

この記事で説明しないこと

deviseを使ったユーザー認証等、独自ドメイン紐付けに関係のない部分

この記事での目標

プラットフォーム上で作成したスクールに独自ドメインでアクセスできるようになること

実装手順

  • スクールのページをサブドメインごとに切り分ける
  • スクールのページに独自ドメインを紐付ける
  • テナントごとにデータを分離する(途中)

STEP1:スクールページをサブドメインごとに切り分ける

ここではユーザーがスクール開設時に登録したサブドメインをもとに、スクールのトップページを表示するということをやっていきます。

1. ルーティングの制御

まずルーティングを設定していきます。
www以外のサブドメインへリクエストがきたらこの範囲のパスを読み込みます!というのを実行する為の設定です。railsのconstrain機能を使って実装します。

参考記事: rails routing constraintsについて
https://y-yagi.tumblr.com/post/92386974040/rails-routing-constraints%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

lib以下にconstarain用のクラスを定義します。
constrain用のクラスで定義したmatches?がtrueの場合、任意のスコープ内にあるパスが採用される。という感じです。

lib/school_domain.rb
class SchoolDomain
  class << self
    def matches?(request)
      matches_subdomain?(request)
    end

    private
      def matches_subdomain?(request)
        # wwwの場合は追加するルーティングにマッチングしないようにしておく
        request.subdomain.present? && request.subdomain != 'www'
      end
  end
end

route.rb
Rails.application.routes.draw do
  #constratinファイルの読み込み
  require Rails.root.join('lib', 'school_domain.rb')

  #constratinでの制御 matches?で該当したパスはここのルーティングが適応される
  constraints SchoolDomain do 
    scope module: :school do
      get '/', to: 'schools#show'
      resources :lessons, only: [:show, :index]
    end
  end

  ...省略

2. サブドメインの抽出とスクールの検索

requestに入ってくるドメインの情報を元にサブドメインを特定します。このとき、wwwがサブドメインの場合は除外しておきたいので、そのロジックも入れます。

このメソッドはbefore_actionで呼び出すのですが、先ほど定義したルーティングないで実行されるコントローラーで呼びだすことが多いと思います。例えば今回の場合は、schools controllerのshowで呼び出します。 

application_controller.rb
    def set_school_by_domain_name
      if request.subdomain.present? && request.subdomain != 'www'
        @school = School.find_by(subdomain: request.subdomain)
      end  

      # 該当するスクールがない場合、URLをリセットしてトップページにリダイレクト
      redirect_to root_url(domain: ENV["MY_DOMAIN_NAME"], subdomain: "") unless @school.present?
    end

ローカルで動作確認
ローカルでの動作確認は2種類方法があります。一つは、etc/hostファイルを変更して任意のURLがきたらlocalhost:3000を向くようにする方法、2つ目は、lvh.me:3000のようなループバックドメインの利用する方法。今回はループバックドメインを使います。

ループバックドメインとは、

メインドメインでアクセスしてもサブドメインでアクセスしても 127.0.0.1 に返してくれるドメイン(サービス)

参照元記事:Railsでユーザごとにサブドメインが切られたユーザページを提供する手順
https://qiita.com/himatani/items/729568277588fcf37720

...実際にやってみる

まず講師登録&ログイン
スクリーンショット 2020-11-29 22.55.19.png

スクール作成
スクリーンショット 2020-11-29 22.56.57.png

スクリーンショット 2020-11-29 22.57.38.png

スクールページへアクセス(ローカル環境: http://helloworld.lvh.me:3000/)
うまく表示できました!
スクリーンショット 2020-11-29 22.58.28.png

試しに適当なURL(helloworld.lvh.me:3000とか)にアクセスすると、講師のダッシュボードトップに戻ってくるので、そっちもうまく行ってそうです。

本番環境での注意点

通常のサービスドメインのSSLに加えて、サブドメインのワイルドカードを持ったSSLを取得していないと全てのサブドメインをSSL通信させることはできません。

特にHerokuでデプロイしている場合は注意が必要です。ACMでサブドメインのSSLを発行できないので、自分で取得または作成してアップロードすることになります。僕はLet'sEncryptoを使ってオレオレ証明書を作成しました。その際に参考にした記事です。

参考記事1: MacOS に CertBot を入れて Let's Encrypt 証明書を作ってみる
https://neos21.hatenablog.com/entry/2019/03/11/080000

参考記事2: Let's Encrypt ワイルドカード証明書の取得手順メモ
https://qiita.com/fukmaru/items/dd75f2a3601472590ead

参考記事3: Heroku SSL + Let's Encrypt で、ワイルドカードなSSL証明書を設定する
https://qiita.com/fukmaru/items/dd75f2a3601472590ead

STEP2:独自ドメインとの紐付け

サブドメインの紐付けがうまくいきました。次は独自ドメインの紐付けです。
まずconstrainの部分を変更して、別ドメインがきた時も同じルーティングを採用するようにします。

lib/school_domain.rb
class SchoolDomain
  class << self
    def matches?(request)
      matches_custom_domain?(request) || matches_subdomain?(request)
    end

    private
      def matches_custom_domain?(request)
        request.domain.present? && request.domain != ENV["MY_DOMAIN_NAME"]
      end

      def matches_subdomain?(request)
        # wwwの場合は追加するルーティングにマッチングしないようにしておく
        request.subdomain.present? && request.subdomain != 'www'
      end
  end
end

次にapplication_controllerでドメインをrequestから抽出して、schoolを検索する処理をします

application_controller.rb
    def set_school_by_domain_name
      if request.domain.present? && request.domain != ENV["MY_DOMAIN_NAME"]
        @school = School.find_by(domain: request.domain)
      end

      if request.subdomain.present? && request.subdomain != 'www'
        @school = School.find_by(subdomain: request.subdomain)
      end  

      # 該当するスクールがない場合、URLをリセットしてトップページにリダイレクト
      redirect_to root_url(domain: ENV["MY_DOMAIN_NAME"], subdomain: "") unless @school.present?
    end

この状態でローカル環境でテストしていきます!

僕の環境ではlvh.meをENV["MY_DOMAIN_NAME"]に設定しているので、別のループバックURL(localtest.me)を独自ドメインとして登録してテストします。
今の時点でlocaltest.me:3000にアクセスしても、正常にroo_urlに戻されました。

スクリーンショット 2020-11-29 23.10.42.png

この状態で、localtest.me:3000にアクセスします
スクリーンショット 2020-11-29 23.15.11.png

うまくいきました!

本番環境で独自ドメインを紐付ける

本番環境の実装方法は、環境によって異なるので今回の記事では割愛します?‍♂️
後日追記するかもしれません。
軽く調べた感じだと「多段CNAME」や「プロキシサーバー」がキーワードになっている印象

ユーザー側で必要な処理

  • 独自ドメインの取得
  • DNSの設定(cnameまたはAレコード) BASEやSHOPIFYのドメイン登録ページがわかりやすい(https://apps.thebase.in/detail/6)

STEP3: テナントごとにデータを分離する

個人情報を取り扱うようなSaaSサービスは、テナントごとにデータを分離することが重要です。データの分離がされていないと何かの拍子に会社Aの情報を会社Bのユーザーに向けて表示してしまう等のリスクが発生します。

apartment gem

「Rails マルチテナンシー」で調べるとapartment gemについての記事がよく出てきます。apartment gemはマルチテナント実装には最適なgemですが、スケールしていく上で落とし穴もあるようです。SmartHRさんが、aparmtnet gemを最初使っていて、途中でCitusData(postgresqlの拡張)に乗り換えるというイベントがありました。詳しくは参考記事をよんでください。

参考記事: SmartHR が定期メンテナンスを始めた理由とやめる理由
https://tech.smarthr.jp/entry/2018/04/06/100000

activerecord-multi-tenant

マルチテナンシーを同一DB上で実現させるためのgemです。テナントごとにテーブルが別れているわけではないので、データの横断は不可能ではないですが、アプリケーションレベルでデータの領域分けを強制させられるのでマルチテナンシーとして機能しそうです(全部は理解できていない汗)

参考記事: ActiverecordMultiTenant でマルチテナンシー by yks0406さん (マルチテナンシーについて、Railsでのアプローチについて一番詳しくまとまっている記事です)
https://note.com/yks0406/n/n09c181400561

参考記事に習って、今回のサンプルアプリもactiverecord-multi-tenantで実装を進めていきます。

スクール(テナント)ごとに生徒の情報を管理する

?‍♂️2020年12月中に追記予定?‍♂️

最後に

SaaSやノーコードアプリを開発するエンジニアのかた増えてきていると思います。
参照記事多目、まとめ記事と言っても過言ではないですが、お役に立てれば幸いです。

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

【RSpec-rails】ユーザーモデル単体テストコード

はじめにrspec-railsfactory_bot_rails、'fakerを導入する

Gemfile
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
end

次にターミナルで$ bundle installrails g rspec installを行う。作成された.rspecファイルに--format documentation(表示の出力がきれいになるようにフォーマットを指示する)を記述

ターミナル
 $ bundle install
 $ rails g rspec install
.rspec
 --format documentation

仕様に沿ったユーザーモデルのデータを設定する

重複したemailは登録できないことにしているため、uniqueを付けて一意となるようにする

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    email { Faker::Internet.unique.free_email }
    password { '123456' }
    password_confirmation { password }
  end
end

factory_botを使う

インスタンス変数userに、先程設定したuserのデータを入れる

spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe '#create' do
    before do
      @user = FactoryBot.build(:user)
    end
  end
end

createアクションをテストする。テスト項目を書き出す

spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe '#create' do
    before do
      @user = FactoryBot.build(:user)
    end

    it 'email, password, password_confirmationが存在すれば登録できること' do
    end

    it 'emailが空では登録できないこと' do
    end

    it '重複したemailが存在する場合登録できないこと' do
    end

    it 'emailには@が含まれてなければ登録できないこと' do
    end

    it 'passwordが空では登録できないこと' do
    end

    it 'passwordが6文字以上であれば登録できること' do
    end

    it 'passwordが5文字以下では登録できないこと' do
    end

    it 'password_confirmationが必須であること' do
    end
  end
end

洗い出した項目を検証するためのコードを書く

RSpec.describe User, type: :model do
  describe '#create' do
    before do
      @user = FactoryBot.build(:user)
    end

    it 'emailが空では登録できないこと' do
      @user.email = ''
      @user.valid?
      expect(@user.errors.full_messages).to include("Email can't be blank")
    end

    it '重複したemailが存在する場合は登録できないこと' do
      @user.save
      another_user = FactoryBot.build(:user)
      another_user.email = @user.email
      another_user.valid?
      expect(another_user.errors.full_messages).to include('Email has already been taken')
    end

    it 'emailに@が含まれていなければ登録できないこと' do
      @user.email = 'sample.com'
      @user.valid?
      expect(@user.errors.full_messages).to include('Email is invalid')
    end

    it 'passwordが空では登録できないこと' do
      @user.password = ''
      @user.valid?
      expect(@user.errors.full_messages).to include("Password can't be blank")
    end

    it 'passwordが6文字以上であれば登録できること' do
      @user.password = "123456"
      expect(@user).to be_valid
    end

    it 'passwordが5文字以下では登録できないこと' do
      @user.password = '12345'
      @user.password_confirmation = '12345'
      @user.valid?
      expect(@user.errors.full_messages).to include('Password is too short (minimum is 6 characters)')
    end

    it 'password_confirmationが空では登録できないこと' do
      @user.password_confirmation = ''
      @user.valid?
      expect(@user.errors.full_messages).to include("Password confirmation doesn't match Password")
    end
  end
end

テストコードを実行する

spec/models/user_spec.rb
# spec/models/user_spec.rbの実行
$ bundle exec rspec spec/models/user_spec.rb
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メタプログラミングRuby(第I部・金曜日まで)を読んでいく

第5章 木曜日:クラス定義

  • JavaやC#におけるクラスの定義は、あなたが「これがオブジェクトに期待する動作です」と言うと、コンパイラが「了解。やってみます」と答える。クラスのオブジェクトを生成して、メソッドを呼び出すまでは何も起こらない。
  • Rubyにおけるクラスの定義は違っており、classキーワードは、オブジェクトの動作を規定しているだけではなく、実際にコードを実行している。
  1. クラスマクロ(クラスを修正する方法)
  2. アラウンドエイリアス(メソッドでコードをラップする方法)

クラス定義

  • クラス定義にはメソッドの定義だけでなく、あらゆるコードを置くことができる。
class MyClass
  puts 'Hello'
end

# => Hello
  • メソッドやブロックと同じように、クラス定義も最後の命令文の値を返す。
result = class MyClass
  self
end

result # => MyClass
  • クラス(やモジュール)定義の中では、クラスがカレンとオブジェクトselfになる。
    • クラスやモジュールは単なるオブジェクトなため、クラスもselfになれる。

カレントクラス

  • Rubyのプログラムは、常にカレントオブジェクトselfを持っている。
  • それと同様に、常にカレントクラス(あるいはカレントモジュール)も持っている。

    • メソッドを定義すると、それはカレントクラスのインスタンスメソッドになる。
  • カレントオブジェクトはselfで参照を獲得できるが、カレントクラスを参照するキーワードはない。

    • カレントクラスを追跡するには、コードを見る。
  • プログラムのトップレベルでは、カレントクラスはmainのクラスのObjectになる

    • トップレベルにメソッドを定義すると、Objectのインスタンスメソッドになるのはこのため。
  • classキーワードでクラス(あるいはmoduleキーワードでモジュール)をオープンすると、そのクラスがカレントクラスになる。

  • メソッドのなかでは、カレントオブジェクトのクラスがカレントクラスになる。

class C
  def m1
    def m2; end
  end
end

class D < C; end

obj = D.new
obj.m1

C.instance_methods(false) # => [:m1, :m2]

class_eval

  • Module#class_eval は、そこにあるクラスのコンテキストでブロックを評価する。
def add_method_to(a_class)
  a_class.class_eval do
    def m; 'Hello!'; end
  end
end

add_method_to String
"abc".m # => "Hello!"
  • Module#class_eval は、BasicObject#instance_eval とはまったく別物。

    • instance_eval はselfを変更するだけだが、 class_evalself とカレントクラスを変更する。
    • class_eval はselfを変更することによって、classキーワードと同じようにクラスを再オープンできる。
    • Module#class_eval はclassよりも柔軟。
      • classが定数を必要とするのに対して、class_eval はクラスを参照している変数なら何でも使える。
    • classは現在の束縛を捨てて、新しいスコープをオープンするのに対し、class_eval はフラットスコープを持っている。
      • つまり「スコープゲート」で学んだように、class_eval ブロックのスコープの外側にある変数も参照できる。
    • instance_evalinstance_exec という双子のメソッドがあるように、 module_evalclass_eval にも、外部のメソッドをブロックに渡せる module_execclass_exec というメソッドがある。
  • クラス以外のオブジェクトをオープンしたいなら、instance_eval を使う。

  • クラス定義をオープンして、defを使ってメソッドを定義したいなら、class_eval を使う。

    • 「このオブジェクトをオープンしたいが、クラスのことは気にしない」なら instance_eval がいいし、「ここでオープンクラスを使いたい」なら class_eval の方がいい。

カレントクラスまとめ

  • Rubyのインタプリタは、常にカレントクラス(あるいはカレントモジュール)の参照を追跡している。defで定義された全てのメソッドは、カレントクラスのインスタンスメソッドになる。
  • クラス定義の中では、カレントオブジェクトselfとカレントクラス(定義しているクラス)は同じである。
  • クラスへの参照を持っていれば、クラスは class_eval (あるいは module_eval )でオープンできる。

クラスインスタンス変数

  • カレントクラスの理論が役立つ。

  • Rubyのインタプリタは、全てのインスタンス変数はカレントオブジェクトselfに属しているが、これはクラスでも同じ。

class MyClass
  @my_var = 1
end
  • クラスのインスタンス変数とクラスのオブジェクトのインスタンス変数は別物
    • 2つの @my_var という名前の変数があるが、それぞれ異なるスコープで定義されており、別々のオブジェクトに属している。
      • クラスインスタンス変数がアクセスできるのはクラスだけであり、クラスのインスタンスやサブクラスからはアクセスできない。
class MyClass
  @my_var = 1
  def self.read; @my_var; end
  def write; @my_var = 2; end
  def read; @my_var; end
end

obj = MyClass.new
obj.read  # => nil
obj.write
obj.read  # => 2
MyClass.read  # => 1

クラス変数

class C
  @@v = 1
end
  • クラス変数はサブクラスや通常のインスタンスメソッドからもアクセスできる。
  • クラス変数はクラスではなく、クラス階層に属している。
class D < C
  def my_method; @@v; end
end

D.new.my_method  # => 1

クラスのトリック

  • 以下のコードをclassキーワードを使わずに書くことができる。
class MyClass < Array
  def my_method
    'Hello!'
  end
end
  • クラスはClassクラスのインスタンスなので、Class.newを呼び出して作ることができる。
    • Class.newは引数(新しいクラスのスーパークラス)と、新しいクラスのコンテキストで評価するブロックを受け取る。
c = Class.new(Array) do
  def my_method
    'Hello!'
  end
end
  • クラス名は単なる定数であるので、上記の無名関数を割り当てることができる。
    • また、Rubyはクラスの名前をつけようとしていることを理解して、定数がClassオブジェクトを参照し、Classオブジェクトも定数を参照するようになる。
c.name # => "MyClass"

特異メソッド

  • 単一のオブジェクトに特化したメソッドのこと。
    • Rubyでは特定のオブジェクトにメソッドを追加できる。
    • 特異メソッドは下記の構文もしくは Object#define_singleton_methods で定義できる。
str = "just a regular string"  # 普通の文字列

def str.title?
  self.upcase == self
end

str.title?  # => false
str.methods.grep(/title/)  # => [:title?]
str.singleton_methods  # => [:title?]

ダックタイピング

  • 静的言語
    • 「あるオブジェクトがTの型を持つのは、それがTクラスに属している。あるいはTインターフェイスを実装している。」
  • 動的言語
    • 「オブジェクトの型はそのクラスとは厳密に結びついていない。型はオブジェクトが反応するメソッドの集合に過ぎない。」
      • 流動的な型のことをダックタイピングと呼ぶ。

クラスメソッド

  • クラスは単なるオブジェクト
  • クラス名は単なる定数

=> クラスのメソッド呼び出しは、オブジェクトのメソッドの呼び出しと同じ。

  • クラスメソッドはクラスの特異メソッド
def obj.a_singleton_method; end
def MyClass.another_class_method; end

def object.method
  # メソッドの中身
end
  • 上記の object の部分には、オブジェクトの参照、クラス名の定数、selfのいずれかが使える。

クラスマクロ

  • Rubyのオブジェクトにはアトリビュートがない。
    • アトリビュートのようなものが欲しいときは、読み取り用と書き込み用の2つのミミックメソッドを定義する。
class MyClass
  def my_attribute=(value)
     @my_attribute = value
  end

  def my_attribute
    @my_attirbute
  end
end

obj = MyClass.new
obj.my_attribute = 'x'
obj.my_attribute  # => x
  • このようなメソッド(アクセサ)を書いていると、すぐに退屈になる。

    • だが、Module#attr_* 族のメソッドを使えば、一気にアクセサを生成できる。
    • Module#attr_reader は読み取り用を生成し、Module#attr_writer は書き込み用を生成し、Module#attr_accessor は読み書き両用を生成する。
  • attr_* 族のメソッドはModuleクラスで定義されているので、selfがモジュールであってもクラスであっても使える。attr_accessorのようなメソッドをクラスマクロと呼ぶ。

  • クラスマクロを使って古い名前を廃止する方法

class Book
  def title # ...

  def subtitle # ...

  def lend_to(user)
    puts "Lending to #{user}"
    # ...
  end

  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warning: #{old_method}() is deprecated. Use #{new_method}()."
      send(new_method, *args, &block)
    end
  end

  deprecate :GetTitle, :title
  deprecate :LEND_TO_USER, :lend_to
  deprecate :title2, :subtitle
end

特異クラス

  • 特異メソッドは、オブジェクトモデルのどこに住むことができない。

    • Rubyのオブジェクトは、裏に特別なクラスを持っており、それはオブジェクトの特異クラスと呼ばれる。
  • Object#class などのメソッドは、特異クラスを丁寧に隠してしまう。

  • しかし、classキーワードを使った特別な構文を使うと、特異クラスのスコープに入ることができる。

class << an_object
  # あなたのコードをここに
end
  • 特異クラスの参照を取得したければ、スコープの外にselfを返せればいい。
obj = Object.new

singleton_class = class << obj
  self
end

singleton_class.class  # => Class
  • または、特異クラスを参照するselfを戻さなくても、 Object#singleton_class という便利なメソッドが使える。
"abc".singleton_class  # => #<Class:#<String:0x3331df0>>
  • 先ほどの例から、特異クラスはクラスであるが、特別なクラスであることがわかる。
    • まず、 Object#singleton_classclass << という変わった構文を使わなければ見ることができない。
    • 次に、インスタンスを一つしか持てない
      • だから特異クラスはシングルトンクラスとも呼ばれる。
    • そして、継承ができない。
    • 最後に、特異クラスはオブジェクトの特異メソッドが住んでいる場所
def obj.my_singleton_method; end
singleton_class.instance_methods.grep(/my_/)  # => [:my_singleton_method]

特異クラスのメソッド探索

  • 特異メソッドも通常のメソッド探索に当てはまる。
    • オブジェクトが特異メソッドを持っていれば、Rubyは通常のクラスではなく、特異クラスのメソッドから探索を始める。
      • これで特異メソッドを呼び出せる。
    • 特異クラスにメソッドがなければ、継承チェーンを上ヘ進み、特異クラスのスーパークラスにたどり着く。
      • これはオブジェクトの通常のクラスになる。

特異クラスと継承

  • obj -> (singleton)class -> #obj
  • #obj(singleton-method(特異メソッド)) -> superclass -> D
  • D -> (singleton)class -> #D
    • -> superclass -> C
  • #D -> superclass -> #C
  • C -> (singleton)class -> #C
    • -> superclass -> Object
  • #C -> superclass -> #Object
  • Object -> (singleton)class -> #Object
    • -> superclass -> BasicObject
  • #Object -> (singleton)class -> #BasicObject
  • BasicObject -> (singleton)class -> #BasicObject
  • #BasicObject -> superclass -> Class

大統一理論

  • 「Rubyのオブジェクトモデルには、クラス、特異クラス、モジュールがある。インスタンスメソッド、クラスメソッド、特異メソッドがある。」
  1. オブジェクトは1種類しかない。それが通常のオブジェクトかモジュールになる。
  2. モジュールは1種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
  3. メソッドは1種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。
  4. 全てのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。
  5. 全てのクラスは(BasicObjectを除いて)ひとつの祖先(スーパークラスかモジュール)を持っている。つまり、あらゆるクラスがBasicObjectに向かって1本の継承チェーンを持っている。
  6. オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである(3回唱えてみよう)。
  7. メソッドを呼び出すときは、Rubyはレシーバの本物のクラスに向かって「右へ」進み、継承チェーンを「上へ」進む。Rubyのメソッド探索について知るべきことは以上だ。
  • Rubyプログラマなら「この複雑な階層で最初に呼ばれるメソッドはどれだろう?」「あのオブジェクトからこのメソッドを呼び出せるだろうか?」といったオブジェクトモデルに関する難しい質問に出会うことがあるだろう。
    • このような質問に出会った時には、先ほどの7つのルールを見返して、オブジェクトモデルの図をさっと描いてみれば、すぐに答えが見つかるはずだ。

クラスメソッドの構文

  • クラスメソッドはクラスの特異クラスにあるメソッド。
# 1. クラス名が重複した定義
def MyClass.a_class_method; end

# 2. クラス定義の中のselfがクラス自身になることをうまく活用している。
class MyClass
  def self.another_class_method; end
end

# 3. 特異クラスをオープンして、そこにメソッドを定義している。(特異クラスを明示的に意識しているため、Ruby界隈で一目置かれるコード)
class MyClass
  class << self
    def yet_another_class_method; end
  end
end

特異クラスとinstance_eval

  • instance_evalself を変更し、 class_evalself とカレントクラスの両方を変更していた。
  • instance_eval もカレントクラスを変更する。
    • カレントクラスをレシーバの特異クラスに変更する。
s1, s2 = "abc", "def"

s1.instance_eval do
  def swoosh!; reverse; end
end

s1.swoosh! # => "cba"
s2.respond_to?(:swoosh!) # => false
  • とはいえ、カレントクラスを変更するために instance_eval を使うことはほとんどない。
    • instance_eval の意味は「 self を変更したい 」のまま。

クラスのアトリビュート

# 動くには動くが、全てのクラスにアトリビュートが追加されてしまう。
class MyClass; end

class Class
  attr_accessor :b
end

MyClass.b = 42
MyClas.b  # => 42

# MyClassにだけアトリビュートを追加するには、特異クラスにアトリビュートを追加する。
class MyClass
  class << self
    attr_accessor :c
  end
end

MyClass.c = 'It works!'
MyClass.c  # => "It works!"

# 特異クラスにメソッドを定義するとクラスメソッドになるので、以下のように書くのと同じ。
def MyClass.c=(value)
  @c = value
end

def MyClass.c
  @c
end

モジュールの不具合

# クラスがモジュールをインクルードすると、モジュールのインスタンスメソッドが手に入るが、
# クラスメソッドはモジュールの特異クラスの中にいる。
module myModule
  def self.my_method; 'hello'; end
end

class MyClass
  include MyModule
end

MyClass.my_method  # NoMethodError!

# my_methodをMyModuleの普通のインスタンスメソッドとして定義し、
# それから、MyClassの特異クラスで、モジュールをインクルードする。
module MyModule
  def my_method; 'hello'; end
end

class MyClass
  class << self
    include MyModule
  end
end

MyClass.my_method  # => "hello"
  • クラスがモジュールをインクルードすると、モジュールのインスタンスメソッドが手に入るが、クラスメソッドはモジュールの特異クラスの中にいる
  • my_methodMyModule の普通のインスタンスメソッドとして定義し、それから、 MyClass の特異クラスで、モジュールをインクルードする。
  • my_methodMyClass の特異クラスのインスタンスメソッドである。つまり、 my_methodMyClass のクラスメソッドである。
    • この技法は クラス拡張 と呼ばれる。

クラスメソッドとinclude

  • クラス拡張を使ってクラスの特異クラスにメソッドを定義すれば、それはクラスメソッドになる。
    • クラスメソッドは特異メソッドの特殊なケースにすぎないので、このトリックはどんなオブジェクトにも適用できる。
  • 普通のオブジェクトに適用した場合、これはオブジェクト拡張と呼ばれる。
module MyModule
  def my_method; 'hello'; end
end

obj = Object.new

class << obj
  include MyModule
end

obj.my_method  # => "hello"
obj.singleton_methods  # => [:my_method]

Object#extend

  • クラス拡張とオブジェクト拡張はよく使われる機能なので、そのためだけにメソッドが提供されている。
  • Object#extend は、レシーバの特異クラスにモジュールをインクルードするためのショートカットである
module MyModule
  def my_method; 'hello!'; end
end

obj = Object.new
obj.extend MyModule
obj.my_method  # => 'hello'

class MyClass
  extend MyModule
end

MyClass.my_method  # => 'hello'

メソッドラッパー

  • メソッドの中にメソッドをラップする方法は3つある。

アラウンドエイリアス

  • Module#alias_method を使えば、Rubyのメソッドにエイリアス(別名)をつけることができる。
    • Rubyには alias キーワードもあり、トップレベルでメソッドにエイリアスをつけるときに便利。
# alias_method
class MyClass
  def my_method; 'my_method()';  end
  alias_method :m, :my_method
end

obj = MyClass.new
obj.my_method # => "my_method()"
obj.m  # => "my_method()"

# alias
class MyClass
  alias_method :m2, :m
end

obj.m2  # => "my_method()"
  • String#sizeString#length のエイリアス。
  • Interger クラスには少なくとも5つの異なる名前を持つメソッドがある。

メソッドの再定義

  • 新しいメソッドを定義して、元のメソッドの名前をつけること。
    • これなら元のメソッドをエイリアスで呼び出すことができる。
class String
  alias_method :real_length, :length

  def length
    real_length > 5 ? 'long' : 'short'
  end
end

"War and Peace".length  # => "long"
"War and Peace".real_length  # => 13
  • Thorというコマンドラインユーティリティを構築するRuby gemの例。
    • Thorにはrake2thorというRakeビルドファイルをThorスクリプトに変換するプログラムが含まれており、その処理の中でrake2thorは、Rakefileをloadして、Rakefileがrquireする全てのファイルを保管する必要がある。
input = ARGV[0] || 'Rakefile'
$requires = []

module Kernel
  def require_with_record(file)
    # グローバルの配列に、Rakefileからrquireされるファイルの名前を保管している。
    # Kernel#callerメソッドで呼び出し側のスタックを取得し、スタックの2番目がrake2thorであれば、
    # スタックの1番目はrequireを呼び出すRakefileになる。
    $requires << file if caller[1] =~ /rake2thor:/
    require_without_record file
  end
  # メソッド名をrequire_with_recordにしているが、
  # 実際にはKernel#requireを変更している。
  alias_method :require_without_record, :require
  alias_method :require, :require_with_record
end
  • アラウンドエイリアスとは、新しいrequireが、古いrequireの「周囲(アラウンド)をラップ」しているトリックのこと。
  • アラウンドエイリアスは3つの手順で書ける。

    1. メソッドにエイリアスをつける。
    2. メソッドを再定義する。
    3. 新しいメソッドから古いメソッドを呼び出す。
  • アラウンドエイリアスの欠点

    1. 新しいメソッド名でクラスを汚染してしまうこと -> メソッドをエイリアスした後にprivateにすれば解決できる。
    2. メソッドの変更を考えていない既存のコードを破壊しかねないこと -> Ruby2.0からは、既存のメソッドの周囲に機能を追加する方法が1つではなく2つ導入された。

Refinementsラッパー

  • Refinementsはクラスのコードにパッチを貼り付けるようなものであるが、アラウンドエイリアスの代わりに使うことができる。

    • リファインしたメソッドからsuperを呼び出すと、元のリファインしていないメソッドが呼び出せる。
  • Stringクラスをリファインして、lengthメソッドの周囲に機能を追加した例

module StringRefinements
  refine String do
    def length
      super > 5 ? 'long' : 'short'
    end
  end
end

using StringRefinements
"War and Peace".length  # => "long"
  • Refinementsラッパーが適用されるのは、ファイルの最後まで(Ruby2.1からはモジュール定義の終わりまで)。
    • あらゆるところに適用されるアラウンドエイリアスよりも、こちらの方が一般的に安全であると言える。

Prependラッパー

  • Module#prependinclude と似ているが、継承チェーンでインクルーダーの上ではなく下にモジュールが挿入されるところが違う。
    • つまり、プリペンどしたモジュールがインクルーダーのメソッドをオーバーライドできる。
      • そして、元のメソッドはsuperで呼び出せる。
module ExplicitString
  def length
    super > 5 ? 'long' : 'short'
  end
end

String.class_eval do
  prepend ExplicitString
end

"War and Peace".length  # => "long"
  • Refinementsラッパーのようにローカルなものではないが、Refinementsラッパーやアラウンドエイリアスよりも明示的で綺麗な方法だとされている。
module AmazonWrapper
  def reviews_of(book)
    start = Time.now
    result = super
    time_taken = Time.now - start
    puts "reviews_of() took more than #{time_taken} seconds" if time_taken > 2
    result
  rescue
    puts "reviews_of() failed"
    []
  end
end

Amazon.class_eval do
  prepend AmazonWrapper
end

クイズ

  • 数値の + 演算子は Fixnum#+ のシンタックスシュガー

    • 1 + 1 と書けば、パーサーが内部で 1.+(1) に変換している。
  • Fixnum#+ を再定義して、結果に常にプラス1する例。

class Fixnum
  alias_method :old_plus, :+

  def +(value)
    self.old_plus(value).old_plus(1)
  end
end

まとめ

  • クラス定義がself(あなたが呼び出したメソッドのデフォルトのレシーバ)とカレントクラス(あなたが定義したメソッドのデフォルトのいばしょ)に与える影響について調べた。
  • 特異メソッドや特異クラスと仲良くなり、オブジェクトモデルとメソッド探索に関する新たな知見を得た。
  • 新魔術をいくつか覚えた。クラスインスタンス変数、クラスマクロ、Prependラッパーなど。

  • 「クラス」は、「クラスやモジュール」の短縮形であり、クラスに関することは全てモジュールにも当てはまる。

    • 例えば、「カレントクラス」はモジュールにも当てはまり、「クラスインスタンス変数」は「モジュールインスタンス変数」にもなる。

第5章 金曜日:コードを記述するコード

Kernel#eval

  • Kernel#eval は、渡されたコード文字列を実行して、その結果を戻す。
array = [10, 20]
element = 30
eval("array << element")  # => [10, 20, 30]

REST Clientの例

  • REST Clientはコード文字列を作成及び評価することで、ループの中で一気に全てを定義している。
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

POSSIBLE_VERBS.each do |m|
  eval <<-end_eval
    def #{m}(path, *args, &b)
      r[path].#{m}(*args, &b)
    end
  end_eval
end
  • ヒアドキュメント
    • 上記のコードで、evalの後に続いているのは通常のRubyの文字列。
    • クオートの代わりに、<<-と任意の終端子(ここではend_eval)で文字列が開始する。そして、その終端子のみが含まれる行で文字列が終了する。
    • このコードが、こうした文字列の代わりになるものを使い、4つのコード文字列を生成してevalした結果、それぞれがget、put、post、deleteになる。

Bindingオブジェクト

  • スコープをオブジェクトにまとめたもの。

    • Bindingを作ってローカルスコープを取得すれば、そのスコープを持ちまわすことができる。
    • evalと一緒に組み合わせて使えば、後からそのスコープでコードを実行することができる。
  • Bindingオブジェクトは、Kernel#bindingメソッドで生成できる。

  • Bindingオブジェクトにはスコープは含まれているが、コードは含まれていないため、ブロックよりも「純粋」なクロージャと考えることができる。

    • 取得したスコープでコードを評価するには、evalの引数にBindingを渡せばいい。
class MyClass
  def my_method
    @x = 1
    binding
  end
end

b = MyClass.new.my_method

eval "@x", b  # => 1

irbの例

  • irbは、標準入力やファイルをパースして、それぞれの行をevalに渡すシンプルなプログラム。

    • こうしたプログラムはコードプロセッサと呼ばれる。
  • irbのソースコードの深いところにあるworspace.rbというファイルでevalを呼び出している。

# statementsはRubyのコード行。
# @bindingはirbがコードを異なるコンテキストで実行するときにこの引数を使う(instance_evalとよく似ている)。
# fileとlineは例外が発生したときにスタックとレースを調整するために使う。
eval(statements, @binding, file, line)

「コード文字列」対「ブロック」

  • Kernel#evalは、eval族の特殊ケースである。

    • class_evalやinstance_evalのようにブロックを受け取るのではなく、コード文字列を評価する
  • instance_evalやclass_evalはコード文字列またはブロックのいずれかを受け取ることができる。

  • 文字列にあるコードは、ブロックにあるコードと大きな違いはない。

    • コード文字列はブロックと同じように、ローカル変数にアクセスすることもできる。
array = ['a', 'b', 'c']
x = 'd'
array.instance_eval "self[1] = x"

array # => ["a", "d", "c"]
  • ブロックとコード文字列はよく似ているので、多くの場合はどちらも使うことができる。
    • しかし、可能であればコード文字列を避けるべき。

evalの問題点

  • コード文字列は、シンタックスハイライトや自動保管といったエディタの機能が使えないことが多い。
  • 誰にでも使えるものだが、コード文字列は読むのも修正するのも難しいことが多い。
  • Rubyは評価するまでコード文字列の構文エラーを報告しない。
    • そのため、実行時に予期せずに失敗するような脆弱性のあるプログラムになる可能性もある。

コードインジェクション

  • Arrayのメソッドを確認する手っ取り早い方法は、evalを使ったユーティリティを書いて、サンプルの配列にメソッドを呼び出して、その結果を確かめること。
    • これを配列探索と呼ぶ。
def explore_array(method)
  code = "['a', 'b', 'c'].#{method}"
  puts "Evaluating: #{code}"
  eval code
end

loop { p explore_array(gets.chomp) }

find_index("b")
# => Evaluating: ['a', 'b', 'c'].find_index('b')
# 1

map! { |e| e.next }
# => Evaluating: ['a', 'b', 'c'].map! {|e| e.next }
# ['b', 'c', 'd']
  • 上記のコードは、悪意あるユーザーが、あなたのコンピュータの脅威になる任意のコードを実行できてしまう。
    • ハードディスクを消去されるかもしれないし、アドレス帳の全ての宛先に情熱的なラブレターを送られてしまうかもしれない。
  • このような脆弱性を吐く行為のことをコードインジェクション攻撃と呼ぶ。
object_id; Dir.glob("*")
# => ['a', 'b', 'c'].object_id; Dir.glob("*")
# => [プライベートな情報がズラズラと表示される]

コードインジェクションから身を守る

  • 悪質なコードを書く方法は無数に存在する。
  • 自分で書いた文字列にだけevalを使うように制限すればいいかもしれないが、複雑なケースになると、文字列がどこから来たかを追跡するのは驚くほど難しい。
  • evalは完全に追放すべきだと唱えるプログラマもいる。
  • evalを追放するのであれば、状況に応じて代替となる技法を探さなければいけない。
    • 動的メソッドと動的ディスパッチによって置き換えることができる。
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

# POSSIBLE_VERBS.each do |m|
#   eval <<-end_eval
#     def #{m}(path, *args, &b)
#       r[path].#{m}(*args, &b)
#     end
#   end_eval
# end

POSSIBLE_VERBS.each do |m|
  define_method m do |path, *args, &b|
    r[path].send(m, *args, &b)
  end
end
# def explore_array(method)
#   code = "['a', 'b', 'c'].#{method}"
#   puts "Evaluating: #{code}"
#   eval code
# end

def explore_array(method, *arguments)
  ['a', 'b', 'c'].send(method, *arguments)
end

オブジェクトの汚染とセーフレベル

  • Rubyは、潜在的に安全ではないオブジェクト(特に外部から来たオブジェクト)に自動的に汚染の印をつけてくれる。
    • 汚染オブジェクトには、ウェブフォーム、ファイル、コマンドライン、さらにはシステム変数から、プログラムが呼び込んだ文字列が含まれる。
  • 汚染された文字列を操作して新しい文字列を作ると、その新しい文字列も汚染される。
# オブジェクトが汚染されているかどうかをtainted?メソッドを呼び出して確認している。

# ユーザー入力を読み込む
user_input = "User input: #{gets()}"
puts user_input.tainted?

# <= x = 1
# => true
  • 全ての文字列が汚染されているかどうかを自分で確認しなければいけないのであれば、安全ではない文字列を自分で追跡するのと大差ない。

    • 一方、Rubyはセーフレベルという考えも一緒に提供している。
  • セーフレベルは、オブジェクトの汚染を上手く補完してくれるもの。

    • セーフレベルを設定する(グローバル変数$SAFEに値を設定する)と、潜在的に危険な操作をある程度は制限できる。
  • セーフレベルはデフォルトの0から3の4つから選択できる。

    • 例えばセーフレベル2では、ファイル操作はほとんど認められていない。
    • 0より大きいセーフレベルでは、Rubyは汚染した文字列を評価できない。
$SAFE = 1
user_input = "User input: #{gets()}"
eval user_input

# <= x = 1
# => SecurityError: Insecure operation - eval
  • 安全性を自分で調整するには、コード文字列を評価する前に明示的に汚染を除去してから( Object#untaint を呼び出す)、あとはセーフレベルに頼って、ディスクアクセスのような危険な操作を抑止すればいい。
  • セーフレベルを慎重に扱えば、eval用に制御した環境を作ることができる。
    • そのような環境をサンドボックスと呼ぶ。

ERBの例

  • 標準ライブラリのERBは、Rubyのデフォルトのテンプレートシステムである。
  • このライブラリはコードプロセッサであり、Rubyをどのようなファイルにも埋め込むことができる。

  • <%= ... %> タグの部分にRubyのコードが含まれている。このテンプレートをERBに渡すと、コードが評価される。

<p><strong>Wake up!</strong>It's a nice sunny <%= Time.new.strftime("%4") %>.</p>
require 'erb'
erb = ERB.new(File.read('template.rhtml'))
erb.run

# => <p><strong>Wake up!</strong>It's a nice sunny Friday.</p>
  • テンプレートから抜き出したRubyのコードを受け取り、それをevalに渡すメソッドがERBのソースにある。
    • new_toplevel は、TOPLEVEL_BINDING のコピーを戻すメソッド。
    • インスタンス変数 @src は、ERBのタグの中身。
    • インスタンス変数 @safe_level は、ユーザーが必要とするセーフレベル。
      • セーフレベルが設定されていなければ、タグの中身がそのまま評価される。
      • セーフレベルが設定されていれば、ERBはサンドボックスを作る。その中で、グローバルのセーフレベルをユーザーの指定と一致させ、Procをクリーンルームにして、別のスコープでコードを実行している($SAFEの新し値が有効なのはProcの中だけ。他のグローバル変数とは違い、callの後に以前の値にリセットされる)。
class ERB
  def result(b=new_toplevel)
    if @safe_level
      proc {
        $SAFE = @safe_level
        eval(@src, b, (@filename || '(erb)'), 0)
      }.call
    else
      eval(@src, b, (@filename || '(erb)'), 0)
    end
  end
end

add_checked_attributeの例1

  • カーネルメソッドであり、コード文字列で書かれ、アトリビュートがnilまたはfalseの時に、実行時例外を発生させる add_checked_attribute メソッドの実装。
def add_checked_attribute(klass, attribute)
  eval "
    class #{klass}
      def #{attribute}={value}
        raise 'Invalid attribute' unless value
         @#{attribute} = value
      end

      def #{attribute}()
        @#{attribute}
      end
    end
  "
end

add_checked_attributeの例2

  • evalを使って定義し、このメソッドが将来的に公開された場合、コードインジェクション攻撃の標的になる。
  • また、コード文字列を使わないメソッドで書き直したほうが、人間の読み手にとって読みやすく、もっと洗練されたものができる。
def add_checked_attribute(klass, attribute)
  klass.class_eval do
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless value
      instance_variable_set("@#{attribute}", value)
    end

    define_method attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

add_checked_attributeの例3

  • ブロックを使ってアトリビュートの妥当性を確認することで、柔軟に妥当性を確認できるようにしたい。
def add_checked_attribute(klass, attribute, &validation)
  klass.class_eval do
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

    define_method attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

add_checked_attributeの例4

  • カーネルメソッドをすべてのクラスで使える クラスマクロ に変更する。
    • add_checked_attribute メソッドを attr_checked メソッドに変更する。
    • attr_checked をあらゆるクラス定義で使うには、ClassまたはModuleのインスタンスメソッドにすればいい。
    • メソッドが実行される時にはクラスがselfの役割を担うので、class_evalを呼び出す必要すらない。
class Class
  def attr_checked(attribute, &validation)
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

    define_method_attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

フックメソッド

  • GUIでマウスクリックのイベントをキャッチするように、クラスの継承、モジュールのクラスへのミックスイン、メソッド定義などのイベントをキャッチする。
  • クラスが継承された時や新しいメソッドを獲得した時に、何らかのコードを実行sルウ。

inheritedメソッド

class String
  def self.inherited(subclass)
    puts "#{self}#{} に継承された"
  end
end

class MyString < String; end

# => String は MyString に継承された
  • inherited はClassのインスタンスメソッド。
    • クラスが継承された時にRubyが呼び出してくれる。
    • Class#inheritedはデフォルトでは何もしないので、上記の例のように自分のコードでオーバーライドして使う。
    • Class#inheritedのようなメソッドは、特定のイベントにフックを掛けることから、 フックメソッド と呼ばれる。

その他のフック

  • Module#included や Module#prepend
module M1
  def self.included(othermod)
    puts "M1 は #{othermod} にインクルードされた"
  end
end

module M2
  def self.prepended(othermod)
    puts "M2 は #{othermod} にプリペンドされた"
  end
end

class C
  include M1
  prepend M2
end

# => M1 は C にインクルードされた
# => M2 は C にプリペンドされた
  • Module#extend

    • Module#extendをオーバーライドすれば、モジュールがオブジェクトを拡張した時にコードを実行できる。
  • Module#method_added、method_removed、method_undefined

module M
  def self.method_added(method)
    puts "新しいメソッド:M##{method}"
  end

  def my_method; end
end

# => 新しいメソッド:M#my_method
  • これらのフックは、オブジェクトのクラスに住むインスタンスメソッドにしか使えない。

    • オブジェクトの得意クラスに住む特異メソッドでは動作しない。
  • 特異メソッドのイベントをキャッチするには、

    • Kernel#singleton_method_added、singleton_method_removed、singleton_method_undefinedを使う。
  • Module#includedは、おそらく最も広く使われているフック。

標準メソッドにプラグイン

  • 「フックメソッド」では、モジュールがインクルードされた時にコードを実行するために、Module#includedをオーバーライドすることを学んだ。
  • だが、そのイベントそのものをプラグインする(= 反対側から操作する)こともできる。
    • includeメソッドでモジュールをインクルードするのだから、Module#includedの代わりにModule#includeをオーバーライドすればいい。
module M; end

class C
  def self.include(*modules)
    puts "Called: C.include(#{modules})"
    super
  end

  include M
end

# => Called: C.include(M)
  • Module#includedのオーバーライドとModule#includeのオーバーライドの重要な違い
    • Module#includedはフックメソッドとして使われるだけなので、デフォルトの実装は空。
    • Module#includeは、モジュールに実際にインクルードしなければならない。
      • そのため、自分で絵作るフックコードからベースとなるModule#includeの実装をsuperを使って呼び出す必要がある。
      • superを忘れると、イベントはキャッチできるが、モジュールのインクルードができなくなってしまう。

VCRの例

  • VCRは、HTTP呼び出しを記録および再生するgem。
    • VCRのRequestクラスは、Normalizers::Bodyモジュールをインクルードしている。
module VCR
  class Request # ...

  include Normalizers::Body
  # ...
  • Bodyモジュールは、HTTPメッセージボディーを扱う body_from などのメソッドを定義している。

    • モジュールをインクルードすると、これらのメソッドがRequestクラスのメソッドになる。
    • つまり、RequestがNormalizers::Bodyをインクルードすることにより、クラスメソッドを手に入れた。
  • 一方クラスがモジュールをインクルードすると、通常はクラスメソッドではなく、インスタンスメソッドが手に入る。

    • Normalizers::Bodyなどのミックスインは、以下のようにインクルーダーのクラスメソッドを定義している。
module VCR
  module Normalizers
    module Body
      def self.included(klass)
        klass.extend ClassMethods
      end

      module ClassMethods
        def body_from(hash_or_string)
          # ...
  • BodyにはClassMethodsと言う名前の内部クラスがあり、そこにbody_fromなどの通常のインスタンスメソッドが定義されている。
  • Bodyにはincludedというフックメソッドがある。

  • RubyがBodyをインクルードすると、一連のイベントがトリガーされる。

    • Rubyが、Bodyのincludedフックを呼び出す。
    • フックがRequestに戻り、ClassMethodsモジュールをエクステンドする。
    • extendメソッドが、ClassMethodsのメソッドをRequestの特異クラスにインクルードする。
  • その結果、body_fromなどのインスタンスメソッドがRequestの特異クラスにミックスインされ、実質、Requestのクラスメソッドになる。

add_checked_attributeの例5

  • add_checked_attributeの例4では、attr_checkedと言う名前のクラスマクロを定義していた。

    • このクラスマクロは、Classのインスタンスメソッドで、すべてのクラスで利用可能。
  • attr_checkedへのアクセスを制限する。

    • CheckedAttributesモジュールをインクルードしたクラスだけがアクセスできるようにしたい
module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def attr_checked(attribute, &validation)
     define_method "#{attribute}=" do |value|
       raise 'Invalid attribute' unless validation.call(value)
       instance_variable_set("@#{attribute}", value)
     end

     define_method_attribute do
       instance_variable_get "@#{attribute}"
     end
    end
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon LinuxインスタンスにRails6をインストールするまでの手順

この記事でやろうとしていること

タイトルの通りですが、Amazon LinuxインスタンスにRails6.0.3をインストールするまでの流れです。
なぜRails6.0.3か?というのは、おなじみ「Railsチュートリアル」の第6版で指定されているバージョンだから、というだけです。

RailsチュートリアルではAWSの統合開発環境 (IDE)「Cloud9」の利用を前提としています。Cloud9はRailsをインストールする上での前提パッケージが既に入っているので、

$ gem install rails -v 6.0.3

と叩くだけですんなりRailsが入ってくれますが、一歩cloud9という温室から外に出るとそうはいきません。

というわけで、何番煎じかはわかりませんが、少なくとも筆者はいろいろググって何とか解決したので、需要はあるはず。さっそく解説に入っていきます。

まず、Amazon Linuxインスタンスの詳細は↓の通り。普通にEC2インスタンスをデプロイする際の一番最初に出てくるやつです。
WS000000.JPG

Gitインストール

まずはGitをインストールする必要があります。
後述の手順で、Gitレポジトリをクローンしていろいろとインストールするためです。

ここは特に迷いなく、

# yum install git

でOKです。

rbenvインストール

rbenvとは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使うことを可能としてくれるツールです。
また、(本記事においてはここが重要なのですが)Rubyのインストールもサポートしてくれます。

基本的にはGitHubのrbenvレポジトリのREADMEに書いている通りに従う流れですが、

Gitレポジトリをクローン
# git clone https://github.com/rbenv/rbenv.git ~/.rbenv
ディレクトリ移動後、ソースからインストール
# cd ~/.rbenv && src/configure && make -C src
warning: gcc not found; using CC=cc
aborted: compiler not found: cc

と、gccがインストールされていないとコケてしまいますので、

# yum install gcc

でインストールしてから、再度ソースインストールしましょう。

# cd ~/.rbenv && src/configure && make -C src

rbenvインストール完了後も引き続きREADMEに従って、

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

とパスを通してから、rbenv init コマンドで初期設定します。

# ~/.rbenv/bin/rbenv init
# Load rbenv automatically by appending
# the following to ~/.bash_profile:

eval "$(rbenv init -)"

と出るので、従いましょう。

# eval "$(rbenv init -)"

パスがしっかり通っていることも確認して、

# source ~/.bash_profile
# echo $PATH
/root/.rbenv/shims:/root/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

renv-doctorスクリプトを使用して、rbenvが正しく設定されていることを確認します。

# curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash
Checking for `rbenv' in PATH: /root/.rbenv/bin/rbenv
Checking for rbenv shims in PATH: OK
Checking `rbenv install' support: not found
Unless you plan to add Ruby versions manually, you should install ruby-build.
Please refer to https://github.com/rbenv/ruby-build#installation

Counting installed Ruby versions: none
  There aren't any Ruby versions installed under `/root/.rbenv/versions'.
  You can install Ruby versions like so: rbenv install 2.2.4
Checking RubyGems settings: OK
Auditing installed plugins: OK

出力結果の3行目に「Checking `rbenv install' support: not found」と出力されています。
これが使えないと最新版のRubyをrbenvでインストールできないので、ここからはruby-buildをインストールします。

ruby-buildインストール

ここからは、GitHubのruby-buildレポジトリのREADMEに書いている通りに従っていきます。
rbenvのディレクトリに「plugins」ディレクトリを作って、レポジトリをクローンしてプルするだけです。

# mkdir -p "$(rbenv root)"/plugins
# git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
# git -C "$(rbenv root)"/plugins/ruby-build pull

Rubyインストール

さあ、これで晴れてrbenvを使ってRubyインストールが出来るようになりました。
まずはインストール可能なRubyのバージョンを確認しましょう。

# rbenv install --list
2.5.8
2.6.6
2.7.2
jruby-9.2.13.0
maglev-1.0.0
mruby-2.1.2
rbx-5.0
truffleruby-20.3.0
truffleruby+graalvm-20.3.0

Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all' to show all local versions.

ここでは最新版の「2.7.2」をインストールすることとします。

# rbenv install 2.7.2
Downloading ruby-2.7.2.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.2.tar.bz2
Installing ruby-2.7.2...

BUILD FAILED (Amazon Linux 2 using ruby-build 20201118)

Inspect or clean up the working tree at /tmp/ruby-build.20201128132117.3587.qONaMm
Results logged to /tmp/ruby-build.20201128132117.3587.log

Last 10 log lines:
        from ./tool/rbinstall.rb:846:in `block (2 levels) in install_default_gem'
        from ./tool/rbinstall.rb:279:in `open_for_install'
        from ./tool/rbinstall.rb:845:in `block in install_default_gem'
        from ./tool/rbinstall.rb:835:in `each'
        from ./tool/rbinstall.rb:835:in `install_default_gem'
        from ./tool/rbinstall.rb:799:in `block in <main>'
        from ./tool/rbinstall.rb:950:in `block in <main>'
        from ./tool/rbinstall.rb:947:in `each'
        from ./tool/rbinstall.rb:947:in `<main>'
make: *** [do-install-all] Error 1

おや、makeでコケていますね。

結論としては「openssl-devel」パッケージが必要なところ、インストールされていなかったことが原因です。

私は正直、ここで小1時間あれやこれや調べて、当てずっぽうで問題解決に至ったのですが、じつは↓にruby-buildに必要な前提パッケージが説明されていました。
公式に勝る情報はないですね。
https://github.com/rbenv/ruby-build/wiki#suggested-build-environment

openssl-develはサクっとyumでインストールしましょう。

# yum install -y openssl-devel

その後、

# rbenv install 2.7.2

で無事にrubyの最新版をインストールすることに成功しました。

rubyのパスが2つある件(未解決)

ここまでの手順でrubyをインストール完了しましたが、パスが通っていません。
ruby本体の在り処を探してみますと・・・

# ll ~/.rbenv/shims/ruby
-rwxr-xr-x 1 root root 385 Nov 28 13:53 /root/.rbenv/shims/ruby

# ll ~/.rbenv/versions/2.7.2/bin/ruby
-rwxr-xr-x 1 root root 184688 Nov 28 13:52 /root/.rbenv/versions/2.7.2/bin/ruby

あれ。2箇所にある。別にシンボリックリンクってわけでもないし。ファイルサイズがかなり違う…
以下のように、どちらもrubyコマンドとして実行できます。

# ~/.rbenv/shims/ruby -v
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]

# ~/.rbenv/versions/2.7.2/bin/ruby -v
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]

ちょっとこの点、ちゃんと調べられていないのですが、rbenvで確認すると後者のパスで認識してるっぽいので、筆者の場合はそちらでパスを通し、Rubyインストールを終了としました。

# rbenv versions
* 2.7.2 (set by /root/.rbenv/version)

# echo 'export PATH="$HOME/.rbenv/versions/2.7.2/bin:$PATH"' >> ~/.bash_profile
# source ~/.bash_profile

Railsインストール

お膳立ては整いましたので、あとはRailsチュートリアルの通り、Rails6.0.3をインストールするだけです。

# gem install rails -v 6.0.3
# rails -v
Rails 6.0.3

お疲れ様でした。

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

【Rails】DBと連携してプルダウン(ドロップダウン)メニューをつくる(勉強中)

SIer企業で働く社会人4年目OLです。
仕事をやめる予定はないのですが、手に職をつけていつか副職ができるといいなと思いWEBアプリ開発の勉強し始めました。(2ヵ月くらい)

練習でWEBアプリ開発をしていますが、初めてつまったのがプルダウンメニューの作成なので勉強がてらまとめてみます。同じように悩んでる人の役に立てたらいいなと思います。

勉強始めたばかりなのでここ違うよ!っていうところがあればご指摘いただけると嬉しいです。

やりたいこと

DB(Gameモデル)に格納しているデータをプルダウン(ドロップダウン)で選択して別のDB(Stageモデル)に保存したい。

プルダウン.png

モデル設計

Gameモデル

カラム
id integer
game_name text

Stageモデル

カラム
id integer
game_id ※ integer
stage_name text

※Stageのgame_idがGameのidを外部参照しているイメージ

form_tagで実装

これまでユーザからの入力は全部form_tagで実現していたので、まずはそれでむりやり実装したみた。

新規登録用ページ(stage_a)のView

<!-- 新規登録アクション(stage_n)にデータ渡す -->
<%= form_tag("/stage_n") do %>
  <div class="form">
    <div class="form-body">
      <p>game_name</p>
      <select name="game_name">
        <%@games.each do |game|%>
          <option value=<%="#{game.game_name}"%>><%=game.game_name%></option>
        <%end%>
      </select>
      <p>stage_name</p>
      <input type="text" name="stage_name">
      <input type="submit" value="保存">
    </div>
  </div>
<% end %>

新規登録用のController

  def stage_a
    @games = Game.all
  end

  def stage_n
    @game = Game.find_by(game_name: params[:game_name])
    @stage = Stage.new(game_id: @game.id,stage_name: params[:stage_name])
    @stage.save
  end

新規登録用ページ(stage_a)のブラウザでの見え方

form_tag.png

期待値通りにできた。のでこれでいいのかとも思ったが、ネットで検索してみるとそもそもモデルに紐づいたユーザ入力はform_tagではなくてform_forを使うのが一般的らしい。

form_forで実装

RailsのインプットはProgateでしかしていないのでform_forは初見だが、見よう見まねで実装してみた。

新規登録用ページ(stage_a)のView

<!-- 新規登録アクション(stage_n)にデータ渡す -->
<%= form_for("@stage", url: {controller: 'home', action: 'stage_n' }) do |f|%>
  <div class="form">
    <div class="form-body">
      <p>game_name</p>
      <%= f.collection_select(:game_id,@games,:id,:game_name)%>
      <p>stage_name</p>
      <%= f.text_field :stage_name%>
      <%= f.submit "保存"%>
    </div>
  </div>
<% end %>

新規登録用のController

  def stage_a
    @games = Game.all
    @stage = Stage.new
  end

  def stage_n
    Stage.create(stage_params)
    redirect_to("/stage_view")
  end

    private
    def stage_params
      params.require(:@stage).permit(:stage_name, :game_id)
    end

新規登録用ページ(stage_a)のブラウザでの見え方

form_for.png

なんとか期待値通りになった。無理にループを自分で実装しなくてもよくてすっきりと記述できた。

form_for("保存するモデルのインスタンス", url: {送信先のアクション}) do |f|

本来なら送信先のアクションについても記載しなくても自動でルーティングしてくれるらしいが、うまくルーティングできなかったため記載した。
プルダウン形式は以下のf.collection.selectで実現できた。

f.collection_select(保存カラム,参照インスタンス,保存する参照先カラム,表示する参照先カラム)

ついでに、通常のテキストボックス(1行)は以下のf.text_field

f.text_field 保存カラム

form_forの使い方については【Rails】form_forの使い方をマスターしよう!のページがとても分かりやすかった。というよりRailsのヘルパーメソッドに基本的にこのサイトにまとめられているので今後参考にしていきたい。

form_withで実装

from_forについて調べていたら、Rails5.1以降はform_withを使用することが推奨されているとのこと。これについても見よう見まねで実装してみた。

新規登録用ページ(stage_a)のView

<!-- 新規登録アクション(stage_n)にデータ渡す -->
<%= form_with(model: @stage, url: {controller: 'home', action: 'stage_n'} ,local: true) do |form|%>
  <div class="form">
    <div class="form-body">
      <p>game_name</p>
      <%= form.collection_select(:game_id,@games,:id,:game_name)%>
      <p>stage_name</p>
      <%= form.text_field :stage_name%>
      <%= form.submit "保存"%>
    </div>
  </div>
<% end %>

新規登録用のController

  def stage_a
    @games = Game.all
    @stage = Stage.new
  end


  def stage_n
    @stage=Stage.new(game_id: params[:stage][:game_id],stage_name: params[:stage][:stage_name] )
    @stage.save
    redirect_to("/stage_view")
  end

新規登録用ページ(stage_a)のブラウザでの見え方

form_with.png

これも他と変わらずに期待値通り。正直form_forとそんなに変わらないじゃんと思った。参考にしたページの記述に似せたためControllerに多少違いはあるが、同じ書き方をしても問題ないと思っている。(試してはいない)

form_tagとform_forとform_withについて

それぞれの使い分けについてもざっくり調べてみた。まず、form_tagform_forについては以下のサイトが参考になった。

Railsのform_for/form_tagの分け方の意図としては、

form_for: 任意のmodelに基づいたformを作るときに使う
form_tag: modelに基づかないformを作るときに使う

ということです。
つまり、あるuserモデルに基づいたuserを作成するときはform_forを使い、
そうではなく、検索窓のような何のモデルにも基づかないformを作りたいときはform_tagを使うのが原則です。

【Rails】form_for/form_tagの違い・使い分けをまとめた

モデルに紐づいたフォームか否かが使い分けの基準と理解した。

そして、form_withform_tagform_forの機能をまとめたヘルパーメソッドである。form_withの使用が推奨されていることから、Rails5.1以降を使用する場合はform_withを使用すべきと考えてよいと思う。

プルダウン同士の連携

プルダウンメニューは作れたが、次は別の問題が発生した。
1つ目のプルダウンを選択したら2つ目のプルダウンで選択できる内容が変更できるような入力フォームを作ろうと考えたが、Viewを更新しないとアクションにデータを渡せない。

これを解決するためにはAjaxを理解する必要があるようだが今は???状態なので調べて理解・実装したら、別の記事でまとめようと思う。

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

swiperを導入したけど画像のレスポンシブがうまくいかないよ

はじめに

Railsで自作アプリを開発中の私。(bootstrapを使用)
swiperを使ってサイトの見た目を良くしたいと思い、さっそく導入することに。

その結果、問題なく動きました。

レスポンシブ対応もしました。

ただ、画像がレスポンシブ対応してくれませんでした。

swiperを使うときswiper-containerにheightを設定しますが、画面サイズが小さくなってもheightが固定なので変な余白が生じてしまいます。
またいい感じに調節しても画面幅を小さくすると、画像が見切れるといった別の問題が発生します。

私は当初、メディアクエリを用いてブレイクポイントごとにheightを変更するゴリ押し方法でこれらの問題を解決していました。(これが普通かもしれません。)

でもせっかくbootstrapを使用しているのに追加でメディアクエリを使うのはなんか嫌だなと思ったり。

そんなとき簡単に解決する方法を見つけました。
共有させていただきます。

こんな人に向けて

1.Rails+bootstrapの開発中でswiperを使用したが、レスポンシブがうまくいかない人。
2.メディアクエリを使いたくない人。
3.難しいコードは分からない初心者の人。

解決方法

top.html.erb
<div class="container">
    <div class="swiper-container">
        <div class="swiper-wrapper">
            <div class="swiper-slide"><%= image_tag 'test1.jpg', class:"img-fluid" %></div> 
            <div class="swiper-slide">Slide 2</div>
            <div class="swiper-slide">Slide 3</div>
            <div class="swiper-slide">Slide 4</div>
        </div>
        <div class="swiper-button-prev swiper-button-white"></div>
        <div class="swiper-button-next swiper-button-white"></div>
    </div>
</div>

重要なのは上から4行目の部分。

 <div class="swiper-slide"><%= image_tag 'test1.jpg', class:"img-fluid" %></div> 

"img-fluid" を画像のクラスに指定すると解決してくれます。

before

application.scss
//swiper内の画像調整のための高さ調節
/* すべてのサイズで適用される設定 */
.swiper-container {
    width: 100%;
    height: 600px;
}
/* lg(991px)以下の際に適用される設定 */
@media screen and (max-width: 991px) {
  .swiper-container {
    height: 400px;
  }
}
/* md(767px)以下の際に適用される設定 */
@media screen and (max-width: 767px) {
  .swiper-container{
    height: 300px;
  }
}
/* sm(543px)以下の際に適用される設定 */
@media screen and (max-width: 543px) {
  .swiper-container{
    height: 270px;
  }
}

after

top.html.erb
<div class="swiper-slide"><%= image_tag 'test1.jpg', class:"img-fluid" %></div>
application.scss
.swiper-container {
    width: 100%;
}

これでコードがすっきりしました。

ぜひ試してみてください。

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

マルチスケールシミュレーション特論

Introduction

マルチスケールシミュレーション特論の講義内容などを自分なりにまとめていく

Lecture

Memo<2020-11-21 土>

唐突にqiitaがPost出来ない,Postしても内容が反映されない状態になったので,環境とかをいじりまくった.

qiita_orgを最新版にしたら,emacsも最新版にしろとか云われ,面倒だから,qiita_orgの最新版だけ消そうとしたら,qiitaごと消して,ほんとにPostできなくなりました.

苦節4時間,ただの勘違いでした...(白目)

只今,深夜3時でございます.

皆も,環境いじる前に,資料とか見て間違えてないか調べようね


  • source ~/school/multi/my_ruby/grad_members_20f/members/evendemiaire/README.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マルチスケールシミュレーション特論

Introduction

マルチスケールシミュレーション特論の講義内容などを自分なりにまとめていく

Lecture

Memo<2020-11-21 土>

唐突にqiitaがPost出来ない,Postしても内容が反映されない状態になったので,環境とかをいじりまくった.

qiita_orgを最新版にしたら,emacsも最新版にしろとか云われ,面倒だから,qiita_orgの最新版だけ消そうとしたら,qiitaごと消して,ほんとにPostできなくなりました.

苦節4時間,ただの勘違いでした...(白目)

只今,深夜3時でございます.

皆も,環境いじる前に,資料とか見て間違えてないか調べようね


  • source ~/school/multi/my_ruby/grad_members_20f/members/evendemiaire/README.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

Toshiです!自分の経過を残すために、個人アプリの制作の記録を残していこうと思います!!

正直、覚えたことを落とし込むのでは習慣を守るのがかなり厳しい、、、、
と言うわけで自分のキャッチアップや技術配信と、自分の個人アプリやPF作成などの成長日記の二本軸で書いていこうと思います!!

そもそも何を作ろうとしているか

私は某プログラミングスクールでruby on railsやJSを学びました(まだ通学中ですが、、)。3ヶ月で一通りの作成要件は完了し、個人アプリ作成に移りました。
今作っているのは、引継ぎや情報管理の場面をメインとした少しBtoB向けの情報統制アプリを作っています。

簡潔に言うと情報管理して、検索とかですぐ見つけられる、「小さなGoogle+Trello」的なイメージです!

今日やったこと

前提として1日目とか言いましたが、アプリの要件定義やペルソナを、画面デザイン、部分的にはDB設計などを行い、この日を迎えています。これらも踏まえた反省とやったことを今回は記載しようと思います。

今日やれたこととしては

・アプリケーション作成
・devise ジェムのインストール
・ログイン画面、新規登録画面のView作成
・ログイン、新規登録機能の作成
・ログイン後のマイページの表示
・未ログインユーザーのリダイレクト

です!とりあえず、目標とするログイン実装と画面の実装はできました。

やっていて思ったこと

ざっとですが、deviseのテンプレート画面をデザインし直してこんな感じのログイン画面を作りました。
スクリーンショット 2020-11-29 19.03.23.png

はっきり言うとほぼ流用です。参考にしたのは@Tamitchaoさんの下記の記事です。
https://qiita.com/Tamitchao/items/8e78d87659794f2b07ee

Googleでデザインを参考にできそうなものは見つけられましたが、作るとなると今の自分では作成の一歩が全くわからないと言う感じでした。。。まだまだHTML/CSSは技術不足なんだと痛感しました。

次にアプリ制作に入った時。ユーザー作成をやろう!!と意気込んだものの、DB設計や画面デザイン、アプリケーションの要件定義が曖昧で序盤の序盤で手が止まってしまいました。
ただ、よかったことはdeviseの扱いがいくら分かっていても、大もとのデザインや設計、要件定義が甘いと何もできないことを知れたことです。自分の計画性を改めて見直しできたことと、今後の実装ついての正しいフローを知ることができました。

まとめ

まとめるとこんな感じです。

①個人アプリ制作のViewファイル作成はHTML/CSSは学んだだけではダメ。実践を積み重ねてこそ身に付くこと。欲を言えば数回、自分でViewファイルやHTML/CSSを自由に作ってみること。(これは機会を見てトライ)
②アプリ機能制作やView作成は、特に初心者の場合はしっかり要件定義やDB設計、アソシエーション、デザインを固めてから。

初日で勉強になること、初めて知ることがたくさんで楽しいです!!引き続き、書いていければと思います!!

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

Rspecのシステムテストでクッキーに関するテストしてみた

はじめに

皆さん始めまして!
駆け出しエンジニアのプージニアことpoo3です!
今回初投稿となるので多めに見ていただけると幸いです。

対象となる読者様

・Rspecのシステムテストでクッキーに関してテストを行いたい方

参考記事

@rokさんの記事
https://qiita.com/rok/items/09f5cab47338d0fa9188

結論:gem 'show_me_the_cookies'を導入しよう

まずはGemfileに 'show_me_the_cookies'を追加

Gemfile.rb
gem 'show_me_the_cookies'

次にみんな大好き 'bundle install' ??

bundle install

rails_helperの中でインストールした'show_me_the_cookies'を使えるようにincludeする
(システムテストの中で使う事を想定しております)

rails_helper.rb
config.include ShowMeTheCookies, :type => :system

上記完了したらシステムテストの中でクッキー弄り放題です?

メソッドなどは下記ご参考ください

https://github.com/nruth/show_me_the_cookies

実際に書いてみたテストコード

Rspec初心者なのでツッコミどころ満載かもしれませんが笑
今回は分かりやすいようにあえてリファクタリングはしてない状態で載せます!

user_login_spec.rb
  feature "remember_me機能" do
    before do
      #ユーザを作成する
      @test_user = FactoryBot.create(:user)
    end

    scenario "チェクボックスON" do
      #ログインする
      visit login_path
      expect(page).to have_content 'Log in'
      fill_in 'session_email', with: @test_user.email
      fill_in 'session_password', with: @test_user.password
      check 'session_remember_me'
      expect(page).to have_checked_field('session_remember_me')
      click_button 'ログイン'
      #永続クッキー作成されているか確認
      show_me_the_cookies
      expect(get_me_the_cookie('remember_token')).to_not eq nil
      #クッキーがきちんと仕事をするか確認
      expire_cookies
      visit root_path
      expect(page).to have_content 'ログアウト'
    end

    scenario "チェックボックスOFF" do
      #ログインする
      visit login_path
      expect(page).to have_content 'Log in'
      fill_in 'session_email', with: @test_user.email
      fill_in 'session_password', with: @test_user.password
      expect(page).to have_unchecked_field('session_remember_me')
      click_button 'ログイン'
      #永続クッキー作成されていない事を確認
      show_me_the_cookies
      expect(get_me_the_cookie('remember_token')).to eq nil
    end
  end

上記コードで使用している'show_me_the_cookies'のメソッド紹介

show_me_the_cookies
現在のクッキー情報を出力する
クッキーの中身の情報を確認したい時に便利!
expire_cookies
期限切れのクッキーとセッションを全て破棄する!!
実質一度ブラウザを閉じた状態にすることができる✌️
get_me_the_cookie(cookie_name)
特定のクッキー情報をハッシュで取得

終わりに

少し短いですが以上になります!
これからも週1回程度何かしらの記事を書けたらと思います?
ここまで読んでくれた方ありがとうございました!

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

reactでonClick実行時に引数を渡す

呼び出すとき

  • bindを使う
  • onClick={this.handleClickDelete.bind(this, "渡したい値")}
<button onClick={this.handleClickDelete.bind(this, pod.SubDomainName)}>Delete</button>

onClickを受け取る側の関数

  • handleClickDelete(SubDomainName)の形で受け取る
  • console.log(SubDomainName)のように使う
    handleClickDelete(SubDomainName) {
        console.log(SubDomainName)
        const json = {"Name":SubDomainName};
        const convert_json = JSON.stringify(json);
        const obj = JSON.parse(convert_json);
        console.log(obj)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】carrierwave(キャリアウエーブ)で動画の投稿(保存) 

はじめに

キャリアウエーブ(carrierwave)を導入しました。
画像ではなく、動画の投稿方法を紹介します。
 

目次

  1. carrierwave導入
  2. 動画の保存準備
  3. 動画の投稿

開発環境

ruby 2.6.5
rails 6.0.0

実装

それでは実装していきます〜

1. carrierwave導入

まずはGemをインストールします。

ターミナル
gem "carrierwave"
ターミナル
bundle install

gemを読み込むために、rails s して必ずサーバーを再起動させてください。

1.2 アップローダークラスの生成

動画専用のファイルを作成します。

ターミナル
rails g uploader video_top

構文はこちらです。

ターミナル
rails g uploader アップローダー名

今回はvideo_tpoという名前で作ります。

そうすると以下の様なファイルが自動生成されます。

スクリーンショット 2020-11-22 5.49.30.png

いろいろ記述してありますが、extension_whitelistの部分をコメントインしてMOV wmv mp4を追記します。

app/uploaders/video_uploader.rb
#省略

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_whitelist
    %w(jpg jpeg gif png MOV wmv mp4) #←ここを追記
  end

#省略

アップロードするファイルの拡張子やサイズ、保存するパスを指定する事が出来ますが、今回は動画なので MOV wmv mp4この3っつを追記します。

ここまでがキャリアウエーブの導入です。

2. 動画の保存準備

続いて動画の保存をします。
MVCの作成をしていきます。

まずはモデル作成します。

ターミナル
rails g model top

今回はtopという名前で作成します。

まず自動生成されてるマイグレーションファイルに追記します。

db/migrate/2020XXXXXXXXX_create_tops.rb
class CreateTops < ActiveRecord::Migration[6.0]
  def change
    create_table :tops do |t|
      t.string :video_top #←ここ

      t.timestamps
    end
  end
end

今回はvideo_topという名前でカラムを追加します。

ターミナル
rails db:migrate

次に、できたモデルに、アップローダークラスとカラムの紐づけをしていきます。

app/models/top.rb
class Top < ApplicationRecord
  mount_uploader :video_top, VideoUploader
end

構文はこちらです。
mount_uploader [:カラム名], [アップローダークラス]

カラム名はvideo_topでアップローダークラスは最初に作成したアップローダークラスのクラス名VideoUploaderです。

コントローラーの作成・編集をします。

ターミナル
rails g controller top index new create

保存と再生に最低限必要なアクションはindex new createです。

自動で記述さてるnew createに追記し、paramsを許可してデータを取得できるようにします。

app/controllers/tops_controller.rb
class TopsController < ApplicationController
  def index
  end

  def new
    @top = Top.new #←ここを追記
  end

  def create
    @top = Top.new(tops_params) #←ここを追記
    @top.save! #←保存だけします 「!」をつける事でエラーを表示してくれます。
  end


  private

#ここからがデータを許可する記述
  def tops_params
    params.require(:top).permit(:video_top)
  end
end

これで保存する裏側は完成です。

あとはビューを作って投稿ができるようにします。

3. 動画の投稿

自動生成されてるviewのnewに追記します。

app/views/tops/new.html.erb
<h1>Tops#new</h1>
<p>Find me in app/views/tops/new.html.erb</p>

#ここから追記
<%= form_with model: @top, local: true do |f| %>

<div class="field">
  <%= f.label :video %>
  <%= f.file_field :video_top, :accept => 'video/*' %>
</div>

<div class="#">
  <%= f.submit "作成する" ,class:"#" %>
  <%=link_to 'もどる', root_path, class:"back-btn" %>
</div>
<% end %>

ルーティングを設定して保存してみます。

config/routes.rb
Rails.application.routes.draw do
  root to: 'tops#index'
  resources :tops
end

http://localhost:3000/tops/new

これで保存機能の完成です!

次に、保存ができたらファイルを確認してみます。

3.1 動画ファイルの確認とgitとの連携

video_topカラムにはファイル名が保存され、動画自体はpublic/top/video_top/「id名」に保存されてます。

スクリーンショット 2020-11-22 7.02.26.png

Githubと連携させている方であれば、commit・pushの際データ量が多すぎるので、Git管理下から除外しときます。

.gitignore
#省略
/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity

/public/uploads #←これを追記

保存が確認されました。

次回、ビューで表示させます。ページに遷移したら(リロード)自動で再生してくれる方法と、ボタンを押して手動再生する方法があるので、二種類紹介します。
よかったら参考にしてみてください。
【Rails】carrierwave(キャリアウエーブ)の再生方法 「自動再生」「手動再生」
後日更新予定

まとめ

キャリアウエーブ(carrierwave)を使った動画の「投稿」方法でした。
下記の参考にせていただいた記事の中に、動画ではなく「画像」を保存してるので記事もあるので、そちらも興味あれば参考にしてみてください。

最後に

私はプログラミング初学者ですが、自分と同じ様にエンジニアを目指す方々の助けになればと思い、記事を投稿しております。
それではまた次回お会いしましょう〜

参考

https://pikawaka.com/rails/carrierwave

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

Nginx+Unicorn(WEB/APサーバーの分離構成)で静的ファイルが読み込めない対処について

アプリをAPサーバー側にデプロイし、Nginxを使ったリバースプロキシ設定を行ったところ、以下画像のようにcssがうまく読み込めない問題が発生したので、その解決までの工程を忘れないようにメモしておきます。

qiita1.PNG

今回の環境

リバースプロキシ.png

  • WEBサーバーにはNginx
  • APサーバーにはUnicorn
  • ruby -v 2.6.6
  • rails -v 5.2.4.4
  • PC Windows10

Google Chromeで検証

Chromeでエラーの出ているページを開いたら、F12か右クリックの検証を選択。
すると以下のようなエラーが確認された。
qiita2.PNG
リソースのロードに失敗したと表示されている。
この時の404は表示させているWEBサーバー側、つまりNginxから404が出ている。
念のため、ログでエラーが出ていないかチェックしたいので、APサーバー側(Unicorn)を調べてみる。

APサーバー(Unicorn)のログ確認

rails配下のlogを確認する。

/var/www/rails/hoge
$ cd log
$tail -f unicorn.log
# -fはファイルの記述を監視。

INFO -- : worker=1 ready
INFO -- : worker=0 ready
INFO -- : reaped #<Process::Status: pid 3997 exit 0> worker=0
INFO -- : reaped #<Process::Status: pid 3998 exit 0> worker=1
INFO -- : master complete
INFO -- : Refreshing Gem list
INFO -- : listening on addr=0.0.0.0:3000 fd=9
INFO -- : master process ready
INFO -- : worker=1 ready
INFO -- : worker=0 ready

一度ブラウザをリロード。
特にそれっぽいエラーはないのでctrl+cで離脱。

WEBサーバー(Nginx)のエラーログ確認

Nginx配下(/etc/nginx)にあるnginx.confをvimで開いてみる。

/etc/nginx
$ sudo vim nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

#error_logの場所が記述されているのでこれをコピー。

$ sudo tail -f /var/log/nginx/error.log

sudo tail -fを叩いてブラウザを一度リロードしてみたが、特にエラーは出なかった。

nginx.confのaccses_logを確認する

先ほどのnginx.confをvimで開いて、accses_logを確認してみる。

/etc/nginx
$ sudo vim nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

access_log  /var/log/nginx/access.log  main;
#access_logの場所が記述されているのでこれをコピー。

$ sudo tail -f /var/log/nginx/access.log

叩いて、ブラウザをリロード。

すると、

"GET /assets/application-03e8ae3db0f190f7147e6299a7a6cb74a90a3f8de03988a2de0595377b02fa60.css HTTP/1.1" 404 1722

assetsの404エラーが確認できた。
railsアプリのpublic配下にassetsがあるので行ってみる。

/var/www/rails/hoge/public
$ cd assets
$ ls

application-03e8ae3db0f190f7147e6299a7a6cb74a90a3f8de03988a2de0595377b02fa60.css

ブラウザのエラーも"03e8"から始まるcssなので一致。

プリコンパイルする際に何かあるっぽい

アプリをデプロイする際、productionでpreconpileしている。

bundle exec rake assets:precompile RAILS_ENV=production

production環境をチェックしたいのでconfig配下のenvironmentsに移動してみる。

/var/www/rails/hoge/config
$ cd environments
$ vim prodaction.rb
#prodaction.rbをvimで開く。

config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
#これが静的ファイルを読み込んでいない原因らしいので環境変数を定義してあげる。

$ export RAILS_SERVE_STATIC_FILES=true
$ env | grep RAILS
$ RAILS_SERVE_STATIC_FILES=true
# 反映されていたらOK

$ unset RAILS_SERVE_STATIC_FILES
# 以前の環境に戻す場合。
  • 補足

present?メソッドはnil, “”, “ “(半角スペースのみ), 空の配列, 空のハッシュが含まれている場合falseつまり、偽とするので元々設定されていない状態だとfalseを返す。
なのでRAILS_SERVE_STATIC_FILESに=falseの状態でもpresent?メソッドは「何か入ってるよ!」つまり真となるのでexport RAILS_SERVE_STATIC_FILES=falseでも結果としてはapサーバ側から静的ファイル
を読み込めるようになるらしい。

UnicornとNginxを再起動させる

Unicornをkillして起動。
Nginxを停止して起動。
これで静的ファイルの読み込みが行われていれば成功。
ブラウザに戻ってWEBサーバーのIPアドレスを叩くと……

qiita3.PNG

やった(´▽`)!

参考

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

RailsのTransactionの使い方について

はじめに

はじめまして。
railsを使用した開発を行っている実務経験1ヶ月のひよっこです。
業務の中でトランザクションの処理の実装を担当させていただく機会がありましたので復習のために投稿させていただきます。
下記投稿の内容は
http://markdaggett.com/blog/2011/12/01/transactions-in-rails/
という内容を一部和訳したものとなっています。

対象

railsを触り始めたばかりの初学者向けの内容となっております。もちろん経験者の方大歓迎です。不足している部分や間違っている部分ありましたらご指摘いただけますと大変嬉しいです。

Transactionの目的

複数のSQL文の変更に対して、全てのアクションが成功した際にDBの変更を発生させるという条件を守らせるために使用するのがTransationの目的です。Transactionにより、データの統一性を保つことができます。
下記のコードは本を100円で購入→本のデータを保存→balance(残高)の保存という3つの処理の例を表しています。この中でどれか一つが失敗する、例えば本を100円で買ったはずがエラーになってしまった。→残高の処理は行われたので所持金は減っているのに本の情報は保存されなかった、などデータの整合性に不具合が生じます。その際にSQL処理を全部ロールバックされるのが、Transactionの特徴です。

この中のどれか一つでも例外が発生するちrollbackしてくれる!
ActiveRecord::Base.transaction do
  book.buy(100) # 本を100円で買うメソッド
  book.save!    # 上記の本の情報が登録される処理
  balance.save! # 自分の残高の保存
end

Transactionはクラスメソッドでもインスタンスでも使える!

Railsでは、トランザクションはすべてのActiveRecordモデルでクラスメソッドとインスタンスメソッドとして利用できます。つまり、以下のアプローチはどちらも同じように有効です。

クラス変数
Client.transaction do
  @client.users.create!
  @user.clients(true).first.destroy!
  Product.first.destroy!
end

インスタンス変数
@client.transaction do
  @client.users.create!
  @user.clients(true).first.destroy!
  Product.first.destroy!
end

上記のコードの中でトランザクションで参照されるモデルクラスがいくつか異なることに気づいたでしょうか?
1つのトランザクション・ブロック内でモデル・タイプが混在していても全く問題ありません。
これは、トランザクションがモデルインスタンスではなくデータベース接続にバインドされているからです。原則として、トランザクションが必要になるのは、複数のレコードへの変更が単一のユニットとして成功しなければならない場合だけです。(ユニットとは、、、本を買う→本の情報保存→自分の残高を減らすという一連の処理のまとまりの事)さらに、Railsはすでに#saveと#destroyメソッドをトランザクションでラップしているので、1つのレコードを更新するときにトランザクションが必要になることはありません。

Transactionのrollbackが発生する条件

rollbackが発生するには、「例外」が必要となります。

上記コードではsaveに!をつけていますが、これがついているメソッドは、失敗したら例外を吐くメソッドです、transactionを使うときは、saveではなくsave!、destroyではなくdestroy!を使うとtransactionがちゃんと拾ってくれます。これをつけないと不整合なデータが保存validationの網をかいくぐって保存されてしまう可能性もあります。

ActiveRecord::Base.transaction do
  david.update_attribute(:amount, david.amount -100)
  mary.update_attribute(:amount, 100)
end

例えば上記のコードではupdate_attributeを使用していますがrailsでは#update_attributeは更新に失敗したときに例外を投げないように設計されています。これはfalseを返すので、使用するメソッドが失敗したときに例外を投げるようにする必要があります。先ほどの例の書き方は次のようになります。

ActiveRecord::Base.transaction do
  david.update_attributes!(:amount => -100)
  mary.update_attributes!(:amount => 100)
end

次の例は#find_byメソッドを使ってトランザクション内でレコードを見つけています。find_byはレコードが返されなかった場合にnilを返すように設計されていますが
通常の #findメソッドは ActiveRecord::RecordNotFound 例外を吐きますよね。

ActiveRecord::Base.transaction do
  david = User.find_by_name("david")
  if(david.id != john.id)
    john.update_attributes!(:amount => -100)
    mary.update_attributes!(:amount => 100)
  end
end

上記のコードは問題があります。どこかわかりますか?

正解はfind_byメソッドの結果がnilであった場合変数davidにnilが代入され、条件分岐をスルーしてしまうところです。
これでは意図しない結果になってしまうでしょう。下記が適切です。

ActiveRecord::Base.transaction do
  david = User.find_by_name("david")
  raise ActiveRecord::RecordNotFound if david.nil?
  if(david.id != john.id)
    john.update_attributes!(:amount => -100)
    mary.update_attributes!(:amount => 100)
  end
end

find_byに対しての例外が発生すると、ロールバックが完了した後、トランザクションの外で例外が発生します。この例外をキャッチし処理をするコードを準備する必要がありますね。

例外を発生させずにロールバックさせる方法

例外を使わずに、Transactionをロールバックさせたい場合は、ActiveRecord::Rollbackを使いましょう。
ActiveRecord::Rollback を使用することで、例外を発生させずにトランザクションを実行することも可能です。これにより、コード内の他の場所でレスキューする必要がなく、トランザクションを無効にしてデータベースレコードをリセットすることができます。

トランザクションを使う上で避けるべきよくあるパターン

単一のレコードのみを更新する
無駄に入れ子にしている
ロールバックが発生しないコードを含むトランザクション
コントローラーでのトランザクションの使用

以上となります。随時新たに知ったこと等あれば追記していこうと思っています。
ここまで見て頂き本当にありがとうございました!

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

rubyzip でZIPを作るときに秒がズレないように日時を入れる方法

Zip::Entry.new の後ろの方に日時を渡すのではなく、インスタンスの time= に渡すと秒がズレないようです

entry = Zip::Entry.new(zos, "foo1.txt", nil, nil, nil, nil, nil, nil, dos_time)

entry = Zip::Entry.new(zos, "foo2.txt")
entry.time = dos_time

下のが検証したコードです

require "zip"
require "zip/version"
require "time"

Zip::VERSION                    # => "2.3.0"

created_at = Time.parse("2020-01-01 00:00:05") # => 2020-01-01 00:00:05 +0900
dos_time = Zip::DOSTime.from_time(created_at)  # => 2020-01-01 00:00:05 +0900

io = Zip::OutputStream.write_buffer do |zos|
  # ズレる方法
  entry = Zip::Entry.new(zos, "foo1.txt", nil, nil, nil, nil, nil, nil, dos_time)
  zos.put_next_entry(entry)

  # ズレない方法
  entry = Zip::Entry.new(zos, "foo2.txt")
  entry.time = dos_time
  zos.put_next_entry(entry)
end
File.write("foo.zip", io.string)
`unzip -o foo.zip`
puts `ls -go --full-time foo*.txt`
# >> -rw-r--r-- 1 0 2020-01-01 00:00:04.000000000 +0900 foo1.txt
# >> -rw-r--r-- 1 0 2020-01-01 00:00:05.000000000 +0900 foo2.txt

参考

rubyzipを使ってファイル圧縮する際に任意の変更日付をいれてテストしてみたら展開ファイルが1秒ズレてハマったという話。
https://qiita.com/kitaindia/items/894a3a600e0892dc0197

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

find_or_initialize_byとfind_or_create_byは何が違う?

はじめに

find_or_initialize_byfind_or_create_byという便利なメソッドがありますが、今まであまり意識せず使っていました。
ふと違いが気になったので調べてみました!

書き方

モデル.find_or_initialize_by(条件)
モデル.find_or_create_by(条件)

両メソッドの違い

find_or_initialize_by
条件を指定して初めの1件を取得し,1件もなければ作成(new)

find_or_create_by
条件を指定して初めの1件を取得し,1件もなければ作成(create)

つまり新規作成して保存するか、しないかです。

ソースコード

既に存在する場合は既存のレコードを取得

User.find_or_initialize_by(name: "花山薫", age: 20)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? AND "users"."age" = ? LIMIT ?  [["name", "花山薫"], ["age", 20], ["LIMIT", 1]]
=> #<User id: 1, name: "花山薫", age: 20, created_at: "2020-11-29 06:45:10", updated_at: "2020-11-29 06:45:10">

User.find_or_create_by(name: "花山薫", age: 20)
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? AND "users"."age" = ? LIMIT ?  [["name", "花山薫"], ["age", 20], ["LIMIT", 1]]
=> #<User id: 1, name: "花山薫", age: 20, created_at: "2020-11-29 06:45:10", updated_at: "2020-11-29 06:45:10">

存在しない場合は作成(new or create)

User.find_or_initialize_by(name: "範馬勇次郎", age: 44)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? AND "users"."age" = ? LIMIT ?  [["name", "範馬勇次郎"], ["age", 44], ["LIMIT", 1]]
=> #<User id: nil, name: "範馬勇次郎", age: 44, created_at: nil, updated_at: nil>

User.find_or_create_by(name: "範馬勇次郎", age: 44)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? AND "users"."age" = ? LIMIT ?  [["name", "範馬勇次郎"], ["age", 44], ["LIMIT", 1]]
   (0.0ms)  begin transaction
  User Create (0.9ms)  INSERT INTO "users" ("name", "age", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "範馬勇次郎"], ["age", 44], ["created_at", "2020-11-29 07:50:49.127558"], ["updated_at", "2020-11-29 07:50:49.127558"]]
   (0.6ms)  commit transaction
=> #<User id: 4, name: "範馬勇次郎", age: 44, created_at: "2020-11-29 07:50:49", updated_at: "2020-11-29 07:50:49">

最後に

何か間違っている点などありましたらご指摘いただけると幸いですm(__)m

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

環境構築 さくっとまとめ(vagrant・CentOS7・rails・mysql8.0)

◇記事の概要

こちらの記事は、環境構築する際にドキュメントを探しにいくのが面倒なので、個人的な備忘録として残したものです。
※必要最低限の設定

その1: Vagrantfileの設定

※vagrant及びvirtualboxがインストールされている前提とします。
まずは、ターミナルを開く。
vagrantfileを置きたいフォルダで以下のコマンドを叩く

$ vagrant init centos/7

フォルダ上にvagrantfileが作成される。
その「Vagrantfile」を修正します。
修正内容は必要最低限の内容になります。

vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
  config.vm.network "private_network", ip: "192.168.33.10"
end

その2:vagrantの起動

vagrantを起動して、ssh接続します。

$ vagrant up

起動後、、

$ vagrant ssh

これで、こんな感じのターミナルが表示される

[vagrant@localhost ~]$

これで一旦vagrant上の作業環境ができた。

その3:rubyのインストール関連

必要なパッケージをインストールする。

[vagrant@localhost ~]$ sudo yum install -y git gcc openssl-devel readline-devel zlib-devel sqlite-devel gcc-c++ libicu-devel cmake vim

次に、rbenvをインストールします。環境変数を追加し、「source ~/.bash_profile」で内容を反映させます。

[vagrant@localhost ~]$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv    
[vagrant@localhost ~]$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
[vagrant@localhost ~]$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
[vagrant@localhost ~]$ source ~/.bash_profile

rbenv --versionと打って、バージョンが表示されれば、OK!

次に、「ruby-build」というプラグインをインストールします。
これは、rbenv installコマンドを使用するために必要なので、下記コマンドを入力します。

こちらの記事より
http://ruby.studio-kingdom.com/rbenv/ruby_build/

[vagrant@localhost ~]$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build

次に、rubyをインストールします。
注意:個人的な都合で、こちらのバージョンを指定しています。

[vagrant@localhost ~]$ rbenv install 2.6.3
[vagrant@localhost ~]$ rbenv global 2.6.3
[vagrant@localhost ~]$ rbenv rehash

ruby -vと打って、バージョンが表示されれば、OK!

最後に、gem同士の互換性を補完するなど、gemを管理してくれる「bundler」をインストールします。

[vagrant@localhost ~]$ gem install bundler
[vagrant@localhost ~]$ rbenv rehash

bundle -v と打って、バージョンが表示されれば、OK!

その4:git clone作業(必要な場合のみ)

今回は、sshを使用して既存のコードをクローンしてきました。

[vagrant@localhost ~]$ cd ~/.ssh
[vagrant@localhost ~]$ ssh-keygen -t rsa
[vagrant@localhost ~]$ sudo cat id_rsa.pub

これで公開鍵・秘密鍵ができます。
公開鍵をgit(今回はbitbucket)に登録し、クローンしました!

その5:Mysql8.0の導入

ここからmysql8.0を使えるようにしていきます。

[vagrant@localhost ~]$ sudo yum install mysql80-community-release-el7-{version-number}.noarch.rpm
[vagrant@localhost ~]$ sudo yum install mysql-community-server
[vagrant@localhost ~]$ sudo systemctl start mysqld
[vagrant@localhost ~]$ sudo grep 'temporary password' /var/log/mysqld.log // 一時的に設定されているパスワードを確認
[vagrant@localhost ~]$ mysql -uroot -p //上記で確認したパスワードを使用
[mysql]$ ALTER USER 'root'@'localhost' IDENTIFIED BY 'ここに新パスワード'; //パスワード変更
[mysql]$ quit;
// bundle installするとmysql関連でエラー出るので、それを回避するために以下のプラグイン等をインストールする
[vagrant@localhost ~]$ sudo yum install mysql-devel 
[vagrant@localhost ~]$ gem install mysql2 -v '0.4.10' --source 'https://rubygems.org/' // ここはクローンしてきたgemに含まれていなかったので。。。
[vagrant@localhost ~]$ bundle install

◇参考
Mysql公式
HARU-LABO

mysqlを使用できるようになるまであと少し。
何が足りないかというと、「database.yml」の設定です。
設定しないと、「rails s」してアクセスするとエラーが出るはず!
参考までに設定ファイル載せておきます。
ファイル設置場所は、app名/config/database.yml

database.yml
default: &default
  adapter: mysql2
  pool: 5
  username: root
  password: 設定したパスワード
  encoding: utf8mb4
  charset: utf8mb4
  collation: utf8mb4_general_ci
  host: localhost

development:
  <<: *default
  database: 開発用DB名

test:
  <<: *default
  database: テスト用DB名
  username: テスト用ユーザー名
  password: テスト用パスワード

production:
  <<: *default
  database: 本番用DB名
  username: 本番用ユーザー名
  password: 本番用パスワード

database.ymlを設定し下記コマンドを入力すると、「database.yml」で設定したdatabaseが作成される。

[vagrant@localhost app名]$ rails db:create

テスト用・本番用は必要に応じて設定。

その6:「rails s」を起動

bundle installもうまくいくはずなので、rails sを行ったところ、以下のエラーが発生。
※一部抜粋

Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable)

というエラー内容。
Javascriptを実行するために必要なプログラムが必要とのこと。
解決策としては、
①「mini_racer」のgemをインストール
②「Node.js」をインストール
前の環境ではNode.jsを利用していたので、②の手順を示します。

// 投稿時点のバージョン
[vagrant@localhost ~]$ curl -sL https://rpm.nodesource.com/setup_15.x | sudo bash -
[vagrant@localhost ~]$ sudo yum install -y nodejs

これで、rails sも実行可能に。

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

Macbookデータ移行したらエラーが出た!?

macbook airからmacbook proにデータ移行してコマンドを打つと、、、

主に実行したコマンドは、

ruby -v

rails -v

などです。

以下のエラーが出ました。

dyld: Library not loaded: /usr/local/opt/gmp/lib/libgmp.10.dylib
  Referenced from: /Users/jun/.rbenv/versions/2.6.5/bin/ruby
  Reason: image not found

なぜなんなんだと、いろいろ試しているうちに解決策が見つかりました。

homebrewのバージョンがおかしかったのか、

brew update && brew upgrade

上記のコマンドでHomebrewを最新の状態にします。

そうすると、エラーが解決し使えるようになりました。

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

Tick-Tack-Toe(どう書く)

問題

http://nabetani.sakura.ne.jp/hena/1/

三目並べ( tick-tack-toe )の手を入力とし、勝敗を出力する。

  • 先攻がo、後攻がx
  • すでに打ってある場所に打った場合、反則負け。 x が反則をした場合、「Foul : o won.」と出力
  • 縦横斜めのいずれかで一列揃ったら、揃えた方の勝ち。 x が揃えた場合、「x won.」と出力
  • 9マス埋まっても揃わなかったら引き分け。 「Draw game.」と出力
  • 勝敗が決した後の手は無視する
  • 入力文字列は、先攻から順に打った位置を示す。盤上の位置と数の対応は下表を参照。
  • 入力文字列が「91593」の場合、「oが9の位置、xが1の位置、oが5の位置、xが9の位置→xの反則負け(最後の3は無視)」となる。
  • 以下の様なケースは考慮しなくてよい。 入力が 1〜9 以外の文字を含んでいる。 入力が不足していて、ゲームの勝敗が決まらない。
1 2 3
4 5 6
7 8 9

コード

Ruby
class TickTackToe
  WinPattern = %W(123 456 789 147 258 369 159 357)
  def initialize(input)
    @input = input.chars.map(&:to_i)
    @field = Array.new(10)
    @turn = 0    #o
    @ptns = WinPattern.map {|pt| pt.chars.map(&:to_i)}
  end

  def solve
    @input.each do |i|
      if @field[i]
        return "Foul : #{output_turn(false)} won."
      else
        @field[i] = @turn
        if settle?
          return "#{output_turn} won."
        elsif @field[1..9].all?
          return "Draw game."  
        else
          @turn = 1 - @turn
        end
      end
    end
  end

  def settle?
    @ptns.any? do |pt|
      a = pt.map {|i| @field[i]}.join
      a == "000" || a == "111"
    end
  end

  def output_turn(t = true)
    %W(o x)[t ? @turn : 1 - @turn]
  end

  def self.solve(input)
    new(input).solve
  end
end


if $0 == __FILE__
  data = [79538246, 35497162193, 61978543, 254961323121, 6134278187, 4319581,
   9625663381, 7975662, 2368799597, 18652368566, 965715, 38745796, 371929,
   758698769, 42683953, 618843927, 36535224, 882973, 653675681, 9729934662,
   972651483927, 5439126787, 142583697, 42198637563, 657391482]

  data.each do |d|
    puts TickTackToe.solve(d.to_s)
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

素因数分解アルゴリズム(Ruby)

はじめに

入力した数字を素因数分解するアルゴリズムを作成します。

実行

入力するためのコードは次のようにします。

puts 数字を入力してください
x = gets.chomp.to_i

素因数分解の為に、別途数字を用意し、その数字で割り切れるかどうかを判別していきます。
割り切れる場合は1をプラスして、次の数字で判別します。

a = 2
puts 数字を入力してください
x = gets.chomp.to_i

ここでは、判定用の数字をaにします。
割り切れるかどうかの判定は次のようにします。

if x % a == 0
end

これで、xがaで割り切れるかどうかが判定できます。
今回は、配列を使用して最後に約数を表示させたいので、割り切れた場合は事前に用意していた配列に追加していきます。

answer = []
if x % a == 0
  answer << a
end

今のままだと、同じ約数がひたすら追加されるだけなので、
割り切れたら元の数字を割る、割り切れなかったら次の数字で試す、
という操作を入れます。

if x % a == 0
  answer << a
  x = x / a
else
  a += 1
end

これを元の数字が1になるまで続けます。

while x > 1
  if x % a == 0
    answer << a
    x = x / a
  else
    a += 1
  end
end

最後にpメソッドを使って表示させます(putsメソッドだと、改行されて結果が見にくくなることがあるので)

p answer

最終的なjコードは次のようになります。

a      = 2
puts 数字を入力してください
x      = gets.chomp.to_i
answer = []

while x > 1
  if x % a == 0
    answer << a
    x = x / a
  else
    a += 1
  end
end

p answer

試しに出力して、775249と入力すると、[61,71,179]と出力されます。
色々改良可能なコードなので、色々試そうと思います。

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

Railsでparticles.jsを使ってみた

はじめに

javascript ライブラリのparticle.jsを使ってみました。
途中、思うようにいかずつまづいていたので、うまくいったやり方を書いておきます。

幾何学模様や、宇宙みたいな粒子をマウスの動きに合わせてアニメーションを設定できる、楽しいやつです。

image.png
snowバージョン
image.png

環境

mac OS バージョン10.15.7
Rails 5.1.6
particles.js 2.0.0

step.1 デモジェネレーターで作りたい見た目を作る

①デモサイトでアニメーションを作る
デモジェネレーターのリンク

https://vincentgarreau.com/particles.js/

で好きなアニメーションを設定する。
カラーはもちろん、アニメーションのスピードや粒子の方向なども設定できる。

②設定したファイルをダウンロード

Download current config (json)

 のボタンでダウンロードできます。

③public/assets 内にダウンロードしたJSONファイルを配置
私はこのファイルを app/assetsに配置してずっとうまくいかないな〜ってなってました。
JSONはimgファイルとかと同じ扱いらしいです。

step.2 パッケージのインストール

Packages install

gem install particles-js-rails
bundle install

step.3 コードを書く

bodyタグの閉じタグ直前に以下を記述

application.html.erb
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
<script>
particlesJS.load('particles-js', 'assets/particles.json', function() {
      console.log('callback - particles.js config loaded');
    });
</script>

assets/particles.jsonの箇所は配置したファイルの名前に合わせる。
ダウンロード直後はparticles-00.jsonのようになっているはず

アニメーションを配置したい箇所に以下を記述

top.html.erb
<div id="particles-js"></div> 

CSSファイルに以下を記述

top.scss
#particles-js {
    width:100%;
    height: 100%;
    margin:0 auto;
    background-color: rgb(230, 182, 216)
}

これで行けるはず。

もしprecompileのエラーが出てきたら、下記を試してみてください

config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( particles.js )

参考(本家)

https://github.com/VincentGarreau/particles.js/
スターを付けて使いましょう。

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

[Ruby] 配列内の偶数を数える

初記事

プログラミング初心者
勉強したことで気になったこと、書き留めておこうと思ったこと、オリジナルアプリ作る際などに記事にしていきます。

初記事は標題の配列内の偶数を数える方法で書きます。

何時ごろか忘れましたがかなり前に同じ問題を解きました。
初めて解くときは途中でわからなくなり解説付きの答えを見ました。

def count_evens(nums)
  count = 0
  nums.each do |num|
    if num.even?
      count += 1
    end     
  end
  puts count
end

※出力例
count_evens([2, 1, 2, 3, 4]) → 3
count_evens([2, 2, 0]) → 3
count_evens([1, 3, 5]) → 0

今は普通に理解できるので簡単に解説すると、配列の中の数字をとりあえず全部出してみてif分で条件に合うものをカウントしたら偶数がカウントできちゃうという仕組み。(ざっくりだけど合ってるよね?)

今回はこの問題に2回目に挑戦。
上記で書いた内容はちょっと忘れてて、何も見ずに問題を解きました。

※今回の回答

def count_evens(num)
  puts num.count{ |i| i.even? }
end 

カウントするからcountメソッド使えばいいか〜でこんな感じ。
理解を深めるためと解説するにあたって少し調べました。
countメソッド 公式リファレンス
リファレンスにも書いてあることですが、countメソッドはブロックを指定した場合、trueの値を数えてくれるというやつ。
初めに見た解説付きの答えと同じ結果が出力されました。(よきよき)

この考えで本当に合ってるかどうかわかりません。何か間違ってるとこなどあればご指摘いただければと。

でわ良き日曜日を:raised_hand:

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

【RSpec】Ruby on Rails チュートリアル「第4章」のテストを RSpec で書いてみた

はじめに

こちらは前回の続きとなります。

【RSpec】Ruby on Rails チュートリアル「第3章」のテストを RSpec で書いてみた
https://qiita.com/t-nagaoka/items/27b51ef1610c4c336a80

対象者

  • Ruby on Rails チュートリアルのテストを Rspec で実施予定、または実施してみたい人
  • Ruby on Rails チュートリアル「第4章」のテストを Rspec で書きたいけど、書き方が分からない人

テストコード

実際にテストコードを書き換えた結果が下記になります。

Minitest

static_pages_controller_test.rb
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end
end

RSpec

static_pages_spec.rb
require "rails_helper"

RSpec.describe "StaticPages", type: :request do
  before do
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  describe "Home page" do
    it "should get home" do
      visit static_pages_home_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "#{@base_title}"
      expect(page).not_to have_title "Home |" # タイトルに「Home |」が含まれていないことを確認
    end
  end

  describe "Help page" do
    it "should get help" do
      visit static_pages_help_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "Help | #{@base_title}"
    end
  end

  describe "About page" do
    it "should get about" do
      visit static_pages_about_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "About | #{@base_title}"
    end
  end
end

ポイント

「 expect(page).to have_title "xxx" 」 だけでは "xxx" がタイトルに含まれていればテストをパスするので、例えタイトルに含まれていて欲しくない "yyy" がタイトルに含まれていてもテストをパスしてしまいます。そのため、タイトルに "xxx" が含まれていて "yyy" がが含まれていないことを確認するためには、上記のエクスペクテーションの他に 「 expect(page).not_to have_title "yyy" 」 を追加で記述し、別途タイトルに "yyy" が含まれていないことを確認する必要があります。

次回

「第5章」のテストコードを RSpec に書き換える予定です。

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

Railsの検索機能を実装する際の変更 -Railsメモ

これから学習してきたことメモする為と、
自分がどういったところにつまづいていたかをメモ

まず、今回は勤怠管理アプリのindexページに対して
ユーザーの名前を"あいまい検索”で探しだし
さらに、通常のh1タイトルが"ユーザー一覧"となっている部分を
あいまい検索を実行した際に"検索結果"というタイトルに変わっているようにする
という内容です。

参考にさせていただいたページはこちらで、

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

deviseの導入の仕方

手順

  1. Gemfileに最下部にgem 'devise'と記述する
  2. bundle installする
  3. rails g devise:installする
  4. モデルを作成するためにrails g devise userを実行する
  5. ビューファイルを設置するためにrails g devise:viewsを実行する

2~5はターミナルで操作します。

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

【Rails devise】deviseの導入と追加するカラムの許可

devise導入の手順

Gemfileでgem 'devise'と追加し、bundle installとしてdeviseのgemを導入


まだdeviseが使えない状態なので、ターミナルでインストールする
$ rails g devise:install
userモデルの生成
$ rails g devise user
ビュー画面のカスタマイズをする場合
$ rails g devise:views
controllerを使ってカスタマイズしたい場合

usersコントローラーの作成

$ rails generate devise:controllers users

 

deviseの便利なメソッド

ログインの有無

user_signed_in?

ログイン中のユーザー

current_user

ログインユーザーのみ許可するメソッド

before_action :authenticate_user!

deviseのusersテーブルに追加するカラムの許可

first_nameとlast_nameの許可

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  private

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name])
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ExcelScanチュートリアル

ExcelScanとは

ExcelScan ( https://github.com/miura1729/excelscan )は、RubyからExcelを簡単に使うためのライブラリです。出来ることや効率を犠牲にすることで、お手軽さを得ています。ExcelScanは基本的にExcelのシートの上から下へ行をスキャンする形で処理を書くためにこう名付けました。
このチュートリアルでは、いくつかの例を元に使い方の概要を説明したいと思います。

材料の集計

あなたは水道屋さんだとします。現場で使った材料とその量を日報に書き込んで行って、毎日家で集計しないといけません。日報の内容はこんな感じでExcelに入力されるでしょう。
image.png
これを集計しようと思います。ピボットテーブルとか駆使すれば簡単かもしれませんが、年寄りにはなかなか覚えられません。そこで、ExcelScanとRubyを使って、こんな感じのプログラムを書きます。

mat.rb```
require 'excelscan'
es = ExcelScan.new("sample/material.xlsx")

table = Hash.new(0)
es.each do |row|
  if row[0] then
    name = "#{row[1]} #{row[2]}"
    table[name] += row[3]
  end
end

es.quit

table.keys.sort.each do |name|
  printf "%-20s  %g\n", name, table[name]
end

これで、集計が出来て、こんな感じでコンソールに出力されます。もちろん、Excelに書きだすこともできますが、ちょっと複雑なのでまた後から説明します。

エルボ φ20               8
エルボ φ25               5
チーズ φ20               2
パイプ φ20               3.5
パイプ φ25               4.3

プログラムが何をやっているのか?Ruby(Excel VBAじゃないのがミソ)になれた人ならすぐ分かるかもしれませんが、改めて説明します。

require 'excelscan'
es = ExcelScan.new("sample/material.xlsx")

requireはExcelScanを使うためのおまじないですね。ExcelScanクラスをnewしてExcelScanオブジェクトを作ります。引数にファイル名を入れるとそのファイルを読み込んでくれます。
他にもシート名とかExcelを画面に表示するかの指定が出来るのですが、今の所は説明しません。

table = Hash.new(0)
es.each do |row|
  if row[0] then
    name = "#{row[1]} #{row[2]}"
    table[name] += row[3]
  end
end

ExcelScanオブジェクトはあたかも二次元配列のように使うことが出来ます。ExcelScan#eachメソッドは各行ごとに順番にその行の行オブジェクトを引数にブロックを実行します。このプログラムではrow変数がそうです。row変数は配列のようなインターフェースでセルの中身をアクセスできます。ちなみに、Excel VBAでは1オリジンですが、ExcelScanではRubyと同じ0オリジンです。セルが数値なら数値が得られますし、文字なら文字が得られます。この辺は変数に型の無いRubyにぴったりですね。
元データのExcelファイルは先頭にヘッダ行があるので、これを集計しないようにしなければなりません。ここでは行番号(1カラム目)が空かどうかで判別します。このあたりは煩雑と言えば煩雑ですが、単純なインタフェースを選択しました。

es.quit

立ち上げたExcelを終了します。何もしなければではRubyのプログラムが終わっても立ち上がったExcelは終了しません。

table.keys.sort.each do |name|
  printf "%-20s  %g\n", name, table[name]
end

集計した結果を出力します。これは、ExcelScan関係ないですね。

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

Ruby で解く AtCoder ARC109 B 二分探索

はじめに

AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。

今回のお題

AtCoder Regular Contest B - log
Difficulty: 524

今回のテーマ、二分探索

入力例 1の説明で AtCoder らしいミスリードがありますが、n + 1 の長さの丸太で 1 ~ k までの短い丸太をつくればヨシ!

Ruby

ruby.rb
n = gets.to_i
m = (1..).bsearch { _1 * (_1.next) / 2 > n.next } - 1
puts n - m + 1
bsearch.rb
m = (1..).bsearch { _1 * (_1.next) / 2 > n.next } - 1

二分探索です。1 ~ k までの短い丸太の長さの合計は、k * (k + 1) / 2 で求めます。bsearchはブロック内の条件がtrueになった最初の値を返しますので、-1 します。

range.rb
(1..Float.INFINITY)
(1..nil) # Ruby 2.6 以降
(1..)    # Ruby 2.7 以降

終端を持たない範囲オブジェクトの指定方法です。AtCoder の環境はRuby2.7ですので、いずれの書き方でもヨシ!

類題 C - Buy an Integer

AtCoder Beginner Contest C - Buy an Integer
Difficulty: 741

ruby.rb
a, b, x = gets.split.map(&:to_i)
y = (1..).bsearch { _1 * a + _1.to_s.size * b > x }
if y.nil?
  puts 0
elsif y > 1000000000
  puts 1000000000
else
  puts y - 1
end

こちらはB - logに比べ、範囲オブジェクトより低い場合と高い場合がありますので少しだけ難しくなっています。

B - log C - Buy an Integer
コード長 (Byte) 84 174
実行時間 (ms) 62 61
メモリ (KB) 14428 14400

まとめ

  • ARC 109 B を解いた
  • Ruby に詳しくなった

参照したサイト
instance method Range#bsearch
class Range

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

Slack API の始め方をまとめました

Slack API の始め方をまとめました

目次

  • SlackにAppを登録
  • Slack App を作成
  • Slack API を利用してメッセージを投稿する。

※ rubyを使用します

SlackにAppを登録

ブラウザで次のページよりSlack APIにログインする。
https://slack.com/signin

開発用のワークスペースを作成

ログインを終えたら次のページより開発テスト用のワークスペースを作成する.
create_workspace.png
まずは、+ Create a New Workspace をクリック。

workspace_setup_1.png
作成するワークスペースの名前を聞かれるので任意の名前を入力し、Nextをクリック。
(筆者は 『development』 という名前に設定)

workspace_setup_2.png
次にワークスペースを利用する目的を聞かれるので任意のメッセージを入力し、Nextをクリック。
(筆者は 『slack api』 と記述)

workspace_setup_3.png
次に招待するメンバーを聞かれるので、無視してSkip this stepをクリック。

workspace_setup_4.png
これでワークスペースの作成が完了。

Slack App を作成する

1. Slack App を作成する

ブラウザで次のページよりSlack Appを作成する。
https://api.slack.com/apps?new_app=1

create_app.png
App Name
アプリの名前を任意に入力
※ 後から変更可能

Development Slack Workspace
アプリを適用するワークスペース(先ほど作成した)を選択
※ 後から変更不可のため注意

入力・選択後Create Appをクリック。

2. ScopesでSlackアプリの権限を設定

set_permission.png
Permissionをクリックし移動

set_scope.png
Add an OAuth Scopeをクリックした後、chat:writeを選択し、Scopeを追加する。
(chat:write)によりBotによる投稿が可能になる
※ 後から追加可能

3. アプリをworkspaceにインストールする

install_app_1.png
Scopeを設定したページと同様のページにて、
Install to Workspaceをクリックし、Slackアプリをワークスペースにインストール。

install_app_2.png
次の様なページが表示されるのでAllowをクリック。
(ワークスペースないでアプリを実行して良いかを確認しています。)

install_app_3.png
これでワークスペースへのアプリのインストールが完了しました。
上記のBot User OAuth Access Tokenは後で使用するのでメモ。

Slack API を利用してメッセージを投稿する

1. Gemfileを追加する

Gemfileを作成する

Gemfile
source 'https://rubygems.org'

gem 'http'
gem 'json'
$ bundle install

2. スクリプト実行用のファイルを用意する

test.rb
require 'http'
require 'json'

BOT_USER_ACCESS_TOKEN="< 先ほどメモしたBot User OAuth Access Token をコピペ >"

response = HTTP.post("https://slack.com/api/chat.postMessage", params: {
  token: BOT_USER_ACCESS_TOKEN,
  channel: '#random',
  text: 'hello slack',
  as_user: true
  })
puts JSON.pretty_generate(JSON.parse(response.body))

解説入り

test.rb
# Apiリクエスト送信に用いる
require 'http'
# Jsonデータ作成に用いる
require 'json'

# ACCESS_TOKENを設定
# Scopeへのアクセスに利用する
BOT_USER_ACCESS_TOKEN="< 先ほどメモしたBot User OAuth Access Token をコピペ >"

# HTTPリクエストを特定にSlackApiのURLに送信する
# chat.postMessage では投稿を作成する
# paramsの中身は指定された値を渡す
response = HTTP.post("https://slack.com/api/chat.postMessage", params: {
  token: BOT_USER_ACCESS_TOKEN,
  channel: '#random',
  text: 'hello slack',
  as_user: true
})
puts JSON.pretty_generate(JSON.parse(response.body))

paramsの中身について

token 認証トークンを指定
channel メッセージを投稿するチャンネルを指定
text メッセージを指定
as_user trueを渡すと認証されたユーザーとしてメッセージが投稿される

詳しくはこちらのページに記載されています。
https://api.slack.com/methods/chat.postMessage

今回はメッセーを投稿するためにchat.postMessageを用いましたが他にも様々なapiメソッドが用意されています。
https://api.slack.com/methods

Botをチャンネルに招待する

invite_bot.png
Apiによるメッセージの送信に用いるチャンネルのメッセージにて
/invite @<Bot_Name>を入力し、Botをチャンネルに招待する。

3. 実行

次のコマンドによりスクリプトを実行

$ bundle exec ruby test.rb
{
  "ok": true,
  "channel": "...",
  "ts": "...",
  "message": {
    "bot_id": "...",
    "type": "message",
    "text": "hello slack",
    "user": "...",
    "ts": "...",
    "team": "...",
    "bot_profile": {
      "id": "...",
      "deleted": false,
      "name": "test_app",
      "updated": ...,
      "app_id": "...",
      "icons": {
        "image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
        "image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
        "image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
      },
      "team_id": "..."
    }
  }

実行結果が上記のようになっていれば成功です!

Slackを確認してみましょう!
success.png
反映されていますね、これにて終了です。
お疲れ様でした :grin:

エラーについて

エラーについて簡単にまとめたので参考までに

channel_not_found チャンネルが存在しない
not_authed 認証トークンが設定されていない、もしくは間違っている
missing_scope スコープ権限が正しく設定されていない設定を変更したらreinstallが必要
not_in_channel チャンネルにBotが追加されていない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】変数展開(数値と文字列の連結)【超基礎】

変数展開について

数値と文字列を連結するには、変数展開を用いる。

age = 26
puts "私は" + age + "歳です"

と入力すると、エラーになる。
正常に動作させるためには、

age = 26
puts "私は#{age}歳です"

と入力すると正常に動作する。

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