20200911のRubyに関する記事は18件です。

rails チュートリアル

4章まで終了
次回5章から

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

【RubyonRails】APIでadd_token_to_usersしてログイン機能を実装する

はじめに

APIでウェブサービスを制作中。
ユーザーのログイン機能を実装することに。
APIのログイン機能作るのは初めてだったので、下記のページを参考に実装。

Railsでトークン認証のログインAPI実装 - Qiita

環境

MacBookAir
ruby 2.6.3
Rails 6.0.3.2

ルーティングを考える

config/routes.rb
Rails.application.routes.draw do
  namespace 'api' do
    namespace 'v1' do
      resources :users, only: [:create, :show, :update]
      post '/users/login', to: 'users#login'
    end
  end
end

マイグレーションの追加

$rails g migration Add_token_To_Users token:token

以下が作成される

db/migrate/20200911133819_add_token_to_users.rb
class AddTokenToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :token, :string
    add_index :users, :token, unique: true
  end
end

モデル

class User < ApplicationRecord
    has_secure_token
end

コントローラー

app/controllers/api/v1/users_controller.rb
module Api
  module V1
    class UsersController < ApplicationController

      def login
        current_user = User.find_by(email: user_params[:email], password_digest: user_params[:password_digest])
        return render json: {status: 401, message: '認証に失敗しました'} unless current_user
        render plain: current_user.token

      rescue StandardError => e
        Rails.logger.error(e.message)
        render json: Init.message(500, e.message), status: 500
      end

      private

      def set_user
        @user = User.find(params[:id])
      end

      def user_params
        params.require(:user).permit(:nick_name, :last_name, :first_name, :email, :password_digest, :zip_code, :prefecture, :city, :house_number, :building_name, :tel)
      end
    end  
  end
end

POSTMANでログインを試してみる

できたっぽい。

Started POST "/api/v1/users/login" for ::1 at 2020-09-11 22:54:07 +0900
Processing by Api::V1::UsersController#login as */*
  Parameters: {"email"=>"aaa@gmail.com", "password_digest"=>"[FILTERED]", "user"=>{"email"=>"aaa@gmail.com", "password_digest"=>"[FILTERED]"}}
  User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`email` = 'aaa@gmail.com' AND `users`.`password_digest` = '1111' LIMIT 1
  ↳ app/controllers/api/v1/users_controller.rb:28:in `login'
Completed 200 OK in 14ms (Views: 0.1ms | ActiveRecord: 1.8ms | Allocations: 11304)

終わりに

これからログアウト機能も実装しないと。

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

ユーザー管理機能をウィザード形式で実装する

概要

今回は、ユーザー管理機能をウィザード形式で実装することに関してです。
ウィザード形式とは、画面遷移しながらユーザー登録を行っていく形式です。

今回、userモデルの登録要件は
nickname, grade, email, password

続いて画面遷移してuser_infoモデルに
subject, school, profile

を登録します。

ちなみに、user_infoは得意教科、在籍校、プロフィールとなっています。

実装

まずはdeviseをインストールして、userモデルを作成します。

emailとpasswordはデフォルトで入っているので、nicknameとgradeを追加します。

カラムを追加したので、次のようにコントローラーを編集します。

controllers.application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname, :grade])
  end
end

次に、userモデルにバリデーションをかけます(コードは省略)

そして、ビューファイルを作ります。

views.devise.registrations.new.html.erb
<h2>ユーザー情報登録</h2>

<%= form_for(@user, url: user_registration_path) do |f| %>
  <%= render "devise/shared/error_messages", resource: @user %>

  <div class="field">
    <%= f.label :nickname %><br />
    <%= f.text_field :nickname %>
  </div>

  <div class="field">
    <%= f.label :grade %><br />
    <%= f.text_field :grade %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

ログイン画面を作ります。

views.home.index.html.erb
<h1>トップページ</h1>

<% if user_signed_in?%>
  <h2>ログインしています</h2>
  <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
<% else %>
  <h2>ログインしていません</h2>
  <%= link_to "新規登録", new_user_registration_path %>
  <%= link_to "ログイン", new_user_session_path %>
<% end %>

画面遷移後のuser_infoモデルを作ります。

この時、外部キーとして、user_idを紐づけるのを忘れないようにします。

その後、userモデルとuser_infoモデルのアソシエーションを組みます。

次はdeviseのコントローラーを作成。

rails g devise:controllers users

次にルーティング。

routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
  root to: "home#index"
end

ここまできたらuser_infoを登録する実装をしていきます。

registrationsコントローラーにnewメソッドを記述し、ビューファイルにformを入れます。

views/devise/registrations/new.html.erb
<%= form_for(@user, url: user_registration_path) do |f| %>
  <%= render "devise/shared/error_messages", resource: @user %>

---省略---

 <div class="actions">
    <%= f.submit "Next" %>
  </div>
<% end %>

ここからが少し記述が増えてきます。
createアクションを記述していきましょう。

controllers/users/registrations_controller.rb
def create
   @user = User.new(sign_up_params)
    unless @user.valid?
      render :new and return
    end
   session["devise.regist_data"] = {user: @user.attributes}
   session["devise.regist_data"][:user]["password"] = params[:user][:password]
   @user_info = @user.build_user_info
   render :new_user_info
 end

sessionを用いて1ページ目に記述したデータを持ってきます。
そしてattributesメソッドでデータを整形しています。

build_user_infoで今回生成したインスタンス@userに紐づくUserInfoモデルのインスタンスを生成します。ここで生成したUserInfoモデルのインスタンスは、@user_infoというインスタンス変数に代入します。そして、住所情報を登録させるページを表示するnew_user_infoアクションのビューへrenderします。

user_infoを登録させるページを表示するnew_user_infoアクションと住所情報を登録するcreate_user_infoアクションのルーティングを設定しましょう。

routes.rb
Rails.application.routes.draw do

  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }
  devise_scope :user do
    get 'user_infos', to: 'users/registrations#new_user_info'
    post 'user_infos', to: 'users/registrations#create_user_info'
  end

  resources :posts

  root to: "home#index"
end

このルーティングに書いた通り、user_infoを登録するビューファイルを作成していきます。

views/devise/registrations/new_user_info.html.erb
<h2>ユーザー情報登録</h2>

<%= form_for @user_info do |f| %>
  <%= render "devise/shared/error_messages", resource: @user_info %>

  <div class="field">
    <%= f.label :subject, "頑張りたい教科" %><br />
    <%= f.text_field :subject %>
  </div>

  <div class="field">
    <%= f.label :school, "学校名" %><br />
    <%= f.text_field :school %>
  </div>

  <div class="field">
    <%= f.label :profile, "自己紹介を記入しましょう" %><br />
    <%= f.text_area :profile %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

このuser_infoを保存する記述をコントローラーにしましょう。

controllers/users/registrations_controller.rb
def create_user_info
    @user = User.new(session["devise.regist_data"]["user"])
    @user_info = UserInfo.new(user_info_params)
      unless @user_info.valid?
        render :new_user_info
      end
    @user.build_user_info(@user_info.attributes)
    @user.save
    session["devise.regist_data"]["user"].clear
    sign_in(:user, @user)
  end

  protected

  def user_info_params
    params.require(:user_info).permit(:subject, :school, :profile)
  end

userをsaveした後に、.clearでsessionを削除しています。
そして、保存された後にログインされるように記述されています。

最後に、create_user_infoアクションに対応するビューを作成します。

views/devise/registrations/create_user_info.html.erb
<h2>登録が完了しました</h2>
<%= link_to "トップへ戻る", root_path%>

以上です。

感想

初めてウィザード形式の登録機能を実装しました。

本当はgradeの学年のところをactive_hashで実装したかったのですが、二時間エラーに悩まされた末に断念しました、、、

一通り実装してみて余裕があったらまたチャレンジしてみます。

日々頑張ってはいるのですが、あまり成長を感じられないです。
めちゃくちゃ記事みないと何もできません、、、

頑張ります。

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

AWSを使ってアプリケーションを公開する手順(6)Nginxを導入する

はじめに

AWSを使ってアプリケーションを公開する手順を記載していく。
この記事ではWebサーバであるNginxを導入する。

Nginxをインストールする

Nginxとは

NginxとはWebサーバの一種である。
ユーザのリクエストに対して静的なコンテンツの取り出し処理を行い、動的なコンテンツの生成をアプリケーションサーバに依頼する。

「.ssh」ディレクトリに移動する

以下のコマンドを実行し、「.ssh」ディレクトリに移動する。

cd ~/.ssh/

ssh接続

以下のコマンドを実行し、EC2インスタンスにsshでアクセスする。
(ダウンロードしたpemファイル名が「xxx.pem」、ElasticIPが123.456.789の場合)

ssh -i xxx.pem ec2-user@123.456.789

Nginxのインストール

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

sudo yum -y install nginx

Nginxの設定ファイルを編集する

以下のコマンドを実行しvimを使ってNginxの設定ファイルを編集する。
/etcディレクトリ以下のファイルは権限がないと読み書き保存ができないため、sudoで実行する。

sudo vim /etc/nginx/conf.d/rails.conf

以下のようにrails.confを編集する。
今回はアプリケーション名が「testapp」、Elastic IPが「123.456.789」の場合を例として進める。

rails.conf
upstream app_server {
  # Unicornと連携させるための設定。
  server unix:/var/www/testapp/tmp/sockets/unicorn.sock;
}

# {}で囲った部分をブロックと呼ぶ。サーバの設定ができる
server {
  # このプログラムが接続を受け付けるポート番号
  listen 80;
  # 接続を受け付けるリクエストURL ここに書いていないURLではアクセスできない
  server_name 123.456.789;

  # クライアントからアップロードされてくるファイルの容量の上限を2ギガに設定。デフォルトは1メガなので大きめにしておく
  client_max_body_size 2g;

# 接続が来た際のrootディレクトリ
  root /var/www/testapp/public;

# assetsファイル(CSSやJavaScriptのファイルなど)にアクセスが来た際に適用される設定
  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }

  error_page 500 502 503 504 /500.html;
}

Nginxの権限を変更する

以下のコマンドを実行してNginxの権限を変更する。
ここで権限を変更することでPOSTメソッドでエラーが出なくなる。

cd /var/lib
sudo chmod -R 775 nginx

「-R」オプション

「-R」オプションは再帰的に変更するためのオプションである。
つまり、そのディレクトリとディレクトリ内の全てのファイルの権限を変更する。

POSTメソッドとは

HTTP通信でクライアントからWebサーバに送るリクエストの一つで、URLで指定したプログラムなどに対してクライアントからデータを送信するためのもの。大きなデータやファイルをサーバに送るために使われる。

Nginxを再起動する

以下のコマンドを実行し、Nginxを再起動し、設定ファイルを再読み込みする。

cd ~
sudo service nginx restart

unicornの設定を修正する

Nginxを介した処理を行うため、unicornの設定を再度修正する。

unicorn.rbを編集する

開発環境でconfig/unicorn.rbを以下のように編集する。
編集したら、コミットとプッシュ忘れずに行う。

config/unicorn.rb
#サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
app_path = File.expand_path('../../', __FILE__)

#アプリケーションサーバの性能を決定する
worker_processes 1

#アプリケーションの設置されているディレクトリを指定
working_directory app_path

#Unicornの起動に必要なファイルの設置場所を指定
pid "#{app_path}/tmp/pids/unicorn.pid"

#ポート番号を指定
listen "#{app_path}/tmp/sockets/unicorn.sock"

#エラーのログを記録するファイルを指定
stderr_path "#{app_path}/log/unicorn.stderr.log"

#通常のログを記録するファイルを指定
stdout_path "#{app_path}/log/unicorn.stdout.log"

#Railsアプリケーションの応答を待つ上限時間を設定
timeout 60

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

本番環境に反映する

以下のコマンドを実行し編集内容を本番環境に反映する。

cd /var/www/testapp(アプリケーションのリポジトリ名が「testapp」の場合)
git pull origin master

unicornを再起動する

以下のコマンドを実行し、unicorn masterのプロセスIDを確認する。

ps aux | grep unicorn

以下のコマンドを実行し、確認したプロセスIDをkillする。
(ここではunicorn masterのプロセスIDが17877であったとする)

kill 17877

以下のコマンドを実行し、unicornを起動する。

RAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -D

ブラウザからElastic IPでアプリケーションにアクセスする

ブラウザからElastic IPでアプリケーションにアクセスする。(今回は:3000をつけなくてよい)
このとき、unicornが起動している必要がある。

エラーが出る場合に確認すること

  • 502 bad gatewayとエラーが出る場合は/var/log/nginx/error.logを確認する
  • /var/www/testapp/log/unicorn.stderr.logを確認しエラーがないか確かめる(リポジトリ名がtestappの場合)
  • railsを起動しているか
  • EC2インスタンスの再起動を行う(本番環境でMySQLとNginxの再起動が必要)

補足

Webサーバとは

外部から送られてきたリクエストを受け取り、処理を加える。
NginxはWebサーバの一種。
以下の役割がある。
- 静的なコンテンツをレスポンスとしてクライアントに返す
- 動的なコンテンツの生成をアプリケーションサーバに依頼する
- アプリケーションサーバから返ってくる処理結果をレスポンスとしてクライアントに返す

静的なコンテンツとは、リクエスト毎に内容が変わらないファイル。
表示するものが決まっているcssや画像ファイルなど。
動的なコンテンツとは、リクエスト毎に内容が変化するファイル。
データベースから検索条件に該当するデータを取得して表示するファイルなど。

アプリケーションサーバとは

動的なコンテンツを生成し、処理結果をWebサーバに返す。
Unicornはアプリケーションサーバの一種。
以下の役割がある。
- Webサーバから依頼された情報を基に動的なコンテンツの生成を行う
- 処理結果をWebサーバに返す

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

セレクトボックスから複数のデータを取り出すまで

Ruby on Railsにおいてセレクトボックスから複数のデータを取り出す作業が意外と複雑な為、これまで試行錯誤した過程をこちらで整理しています。
まだ解決済みではありませんので、進捗が有り次第更新していきます。

バージョン情報

ruby 2.6.4p104
RubyGems 3.0.3
Rails 5.2.3

やりたいこと

現在語学学習系SNSサイトを制作しています。今回はプロフィール編集画面で、ユーザーが学習中の言語を複数選択出来るセレクトボックスを作成したいと思います。
最終的には世界中の言語が網羅されているgemを使おうと思うのですが、まずはテスト段階として「日本語、英語、中国語、スペイン語」の4言語が入ったセレクトボックスを作っていきます。

多対多の関係性

「ユーザー」と「言語」は多対多の関係にあると思いますので、「user_language」と言う中間テーブルを用い「user」と「language」を多対多の関係で結びました。

Userモデル

models/user.rb
class User < ApplicationRecord
 has_secure_password
 has_many :languages, through: :user_language
 has_many :user_language  

 def language_name
   ::LanguageSelect::LANGUAGES[language]
 end
end

Languageモデル

models/language.rb
class Language < ApplicationRecord
  has_many :users, through: :user_language
  has_many :user_language  
end

UserモデルとLanguageモデルの中間テーブル

models/user_language.rb
class Language < ApplicationRecord
  belongs_to :user
  belongs_to :language 
end

schema

db/schema.rb
ActiveRecord::Schema.define(version: 2020_05_21_034306) do

  create_table "languages", force: :cascade do |t|
    t.string "description"
    t.boolean "done"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_languages_on_user_id"
  end

  create_table "user_languages", force: :cascade do |t|
    t.integer "user_id"
    t.integer "language_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["language_id"], name: "index_user_languages_on_language_id"
    t.index ["user_id"], name: "index_user_languages_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "image_name"
    t.string "password_digest"
    t.string "cover_image_name"
    t.string "sex"
    t.string "country"
    t.string "language"
    t.text "introduction"
  end

プロフィール編集ページは以下の様に記述。

views/users/edit.html.erb
  <div class="col-1">Spoken language:</div>
  <div class="col-2">
      <select name="language[]" multiple="multiple" class="form-control">
        <option>日本語</option>
        <option>英語</option>
        <option>中国語</option>
        <option>スペイン語</option>
      <input type="button" value="+" class="add pluralBtn">
      <input type="button" value="-" class="del pluralBtn">
  </div>

コントローラは以下の様に記述。

controllers/users_controller.rb
  def update
    @user = User.find_by(id: params[:id])    
    @user.languages = params[:language]
    if @user.save
      flash[:notice] = "ユーザー情報を編集しました"
      redirect_to("/users/#{@user.id}")
    else
      render("users/edit")
    end
  end

最後にプロフィールページは以下の様に記述。

views/users/show.html.erb
    <div class="language">
    <% @user.languages.each do |language| %>
      <%= @user.language %>
    <% end %>
    </div>

HasManyThroughOrderのエラー

準備は整ったので複数の言語を選択し保存しようとすると、以下のエラーが発生しました。

ActiveRecord::HasManyThroughOrderError in UsersController#update
Cannot have a has_many :through association 'User#languages' which goes through 'User#user_language' before the through association is defined.
Extracted source (around line #125):

    @user.languages = params[:language]

多対多の関係を記述する際に、中間テーブルに関するコード(ここではuser_language)を先に書かないと上記のエラーが発生するようです。
こちらの記事を参考にして、リレーションについて以下の様に記述し直しました。

Userモデル

models/user.rb
class User < ApplicationRecord
 has_secure_password
 has_many :user_language
 has_many :languages, through: :user_language

 def language_name
   ::LanguageSelect::LANGUAGES[language]
 end
end

Languageモデル

models/language.rb
class Language < ApplicationRecord
  has_many :user_language
  has_many :users, through: :user_language
end

AssociationTypeMissmatchのエラー

書き直す事でHasManyThroughOrderのエラーは表示されなくなりましたが、今度は以下のエラーが表示されました。

ActiveRecord::AssociationTypeMismatch in UsersController#update
Language(#135869460) expected, got "英語" which is an instance of String(#21954020)
    @user.languages = params[:language]

Languageのインスタンスを受け取るつもりが"英語"と言う文字列を受け取ってしまった、と言う内容のようです。
なのでそれを防ぐために、プロフィール編集ページにおける選択肢の言語それぞれにvalue属性を付与する事にしました。

views/users/edit.html.erb
  <div class="col-1">Spoken language:</div>
  <div class="col-2">
      <select name="language[]" multiple="multiple" class="form-control">
        <option>日本語</option>
        <option>英語</option>
        <option>中国語</option>
        <option>スペイン語</option>
      <input type="button" value="+" class="add pluralBtn">
      <input type="button" value="-" class="del pluralBtn">
  </div>

こうだった物を、以下の様に変更。

views/users/edit.html.erb
  <div class="col-1">Spoken language:</div>
  <div class="col-2">
      <select name="language[]" multiple="multiple" class="form-control">
        <option value="1">日本語</option>
        <option value="2">英語</option>
        <option value="3">中国語</option>
        <option value="4">スペイン語</option>
      <input type="button" value="+" class="add pluralBtn">
      <input type="button" value="-" class="del pluralBtn">
  </div>

再度AssociationTypeMissmatchのエラー

これで解決したかと思いきや、再び同じエラー文が。

ActiveRecord::AssociationTypeMismatch in UsersController#update
Language(#128573540) expected, got "2" which is an instance of String(#21954020)
    @user.languages = params[:language]

今回こそはlanguage_idを渡したつもりが、ここでもダブルクォーテーション付きの"2"と言う「文字列」として渡ってしまっているようです。

実はこれは、Railsのvalueで送れるデータは文字列限定であると言う事が恐らくの原因でした。

中々上手くいかないとラジオボタンなどで済ませてしまいたくなる気持ちもありますが、最終的には100個以上の言語を選択肢に入れたいので、やはりここはセレクトボックスにこだわって頑張っていきます。

今後

取りあえず今はここで躓いている状態です。
細かく整理しますと、コントローラにおいてユーザーが学習中の言語の情報を以下の様に「@user.languages」と定義した場合はAssociationTypeMismatchのエラーが表示されます。

controllers/users_controller.rb
  def update
    @user = User.find_by(id: params[:id])    
    @user.languages = params[:language]
    if @user.save
      flash[:notice] = "ユーザー情報を編集しました"
      redirect_to("/users/#{@user.id}")
    else
      render("users/edit")
    end
  end

また以下の様に「@user.language」と単数形で定義した場合はエラーは表示されないものの、["1", "2"]のように配列の形のまま(文字列として)出力されます。

controllers/users_controller.rb
  def update
    @user = User.find_by(id: params[:id])    
    @user.language = params[:language]
    if @user.save
      flash[:notice] = "ユーザー情報を編集しました"
      redirect_to("/users/#{@user.id}")
    else
      render("users/edit")
    end
  end

日本語 中国語のように、[]や""を含まない形でデータを個別に取り出し表示させるべく、解決方法を考えていきます。
今考えている方法は2つ:

方法その1
["1", "2"]と言った文字列を受け取った後に数値型に変換する。

方法その2
データを受け取る前に数値型を指定出来るのなら指定する。

2つともどのように記述していけば良いか分からず現在試行錯誤している状況です。
解決策を期待して読んで下さった方には申し訳ありませんが、進捗が有り次第更新していきます。

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

errors.full_messagesの配列を文字に変えて出力する

起こった問題とやりたいこと

errors.full_messagesが配列なので、flashメッセージが、["hoge hoge"]が出力される
→ 文字に変えて、hogehogeと出力したい

やったこと

errors.full_messagesにto_sentenceメソッドをつけるだけ

user_contorller.rb
class Users::UserController < Users::ApplicationController
#中略

def update
 @user = user.Users
flash[:alert] = @user.errors.full_messages.to_sentence
 end

#中略
end

今日もお疲れ様でした。

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

No route matches [GET] "/logout"の解決

はじめに

プログラミング初学者です。
No route matches [GET] "/logout"となりログアウトリンクがうまく動かなかった時の対処法を記載します。

環境

Rails 6.0.3.2
ruby 2.7.1
ubuntu 18.04 LTS

問題点

ログアウトのリンクを押すとNo route matches [GET] "/logout"とでてログアウトできない。

確認事項

・ルーティングのリクエストがdeleteとなっているか
・リンクをmethod: :deleteとしているか
・htmlソースコードがrel="nofollow" data-method="delete"となっているか

解決方法

調べてみるとJQueryがないと動かないらしい

app/javascript/pasks/application.jsに

//= require jquery 
//= require jquery_ujs

と記入

gem 'jquery-rails' 

gemfileからjquery-railsをインストール

解決できました。

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

【Rails】「&.」あ!それみたことある!!(意味は知らない)

 「&.」とは(概要)

ぼっち演算子と呼ばれます。

Rubyでは通常、レシーバーに対してメソッドが実行された時、レシーバー(オブジェクト)がnilだった場合にエラーを返します。

しかしプログラムによっては、エラーを返したくない時があります。そんな時に使うのが「&.」(ぼっち演算子)です。

ぼっち演算子はオブジェクトがnilだった場合にエラーではなく、nilを返してくれます。

 「&.」は何が嬉しい??

返り値が「〇〇かnil」を返すみたいなメソッドがRubyにはあります。それを受け取ったレシーバーに、レシーバーに「nilが想定されていないメソッド」を使ってしまうと、エラーが出力されてしまいます。

そんな時に「&.」を使うことで、そのメソッドはエラーではなく、nilを返してくれるようになります。

 「&.」の懸念点

ぼっち演算子を使ったからと言って、確実にnilを返してくれるというわけではありません。そのメソッド名が存在しない(スペルミス等)場合にはNoMethodErrorが出力されてしまいます。

ぼっち演算子は「レシーバがnilでもエラーを返さない」というだけなので上記のような要因からくるエラーは防げません。

 基本的な使い方

オブジェクト&.メソッド

基本的な形は上記の通りです。

上手な例が思いつかなかったのですが、下記の記事がいい感じだったので拝借します。
https://qiita.com/yoshi_4/items/e987b698c1978d248cfc

@nickname = current_user.nickname
@nickname = current_user&.nickname

前者では、ログインしていない場合、エラーが出てしまいます。currrent_userがnilだからです。
後者では、ぼっち演算子を使っているのでエラーははかれません。

 まとめ

ともかく

  • 「ぼっち演算子」を使うと、レシーバーがnilでもエラーではなく、nilが返される
  • メソッドが存在しない(スペルミス)場合はエラーが出力されてしまう

という感じでしょうか。ご指摘等ありましたら、よろしくお願いします?‍♂️

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

【Ruby on Rails】model,controllerターミナルコマンド

概要

個人的によく使うmodelとcontrollerのコマンドをまとめてみました。
コピペして使えるよう「$」の記述はなくしています。
メモ程度なので説明は最小限にします。
適宜更新していきます。

model、table

【作成】
rails g model Post user:references body:string genre:integer
【モデル、テーブルを削除】
rails destroy Post
【テーブルのみ削除】
rails g migration DropPosts
【テーブル名変更】
rails g migration RenamePostsToBooks

カラム

【カラム追加】
rails g migration AddNameToPosts name:string price:integer

他にも追加したい時
migrationファイルに追加
add_column :posts, :body, :text

index追加時
【単数】
add_index :posts, :body, :unique => true
【複数】
add_index :posts, [:body, :price], :unique => true

【カラム削除】
rails g migration RemoveNameFromPosts name:string price:integer
【データ型変更】
rails g migration ChangeDataNameToPosts name:text
【null追加】
rails g migration change_column_null :posts, :body, false
【カラム名変更】
rails g migration RenamePriceColumnToPosts 

migrationファイルに追加
rename_column :posts, :price, :変更後のカラム名

migration

【実行】
rails db:migrate
【1つ前のバージョンに戻す】
rails db:rollback
【数字分前のバージョンに戻す】
rails db:rollback STEP=3
【データベースの情報をリセット】
rails db:reset
【データベースとmigrationをリセットし、再度migrateする】
rails db:migrate:reset
【migrationのバージョン確認】
rails db:migrate:status

controller

【作成】shopはディレクトリを分けている場合使用。
rails g controller shop::posts new
【削除】
rails destroy controller shop::posts

開発環境

ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina

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

Hanamiでmysqlのsocketを指定する

こうやる

Hanami::Modelは内部的にROM(rubyのorm)を呼び出している

query parameterで渡せる

DATABASE_URL="mysql2://root:password@host/database?socket=/mysql/mysql.sock"

参考

https://rom-rb.org/learn/sql/3.2/#mysql

Cloud RunでHanami

今回 GCPのcloud runでcloud sql と接続するのにsocketを指定する必要があった

cloud sql proxy経由で接続する

DATABASE_URL="mysql2://root:password@host/database?socket=/cloudsql/<DATABASE_CONNECTION_NAME>"

Hanamiは文献が少ないので積極的にアウトプットしていこう

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

Rubyの細かい文法: 同名の変数とメソッドがある場合

Ruby では変数とメソッドに同じ名前をつけられます。同名の変数とメソッドがある場合、変数のほうが優先されます。定義の順序とは関係ありません。

() を付ければ、メソッドと見なされます。

def foo
  "method"
end

foo = "variable"

# これは変数
foo #=> "variable"
# これはメソッド呼び出し
foo() #=> "method"

. を付けてレシーバを指定したときも、メソッドと見なされます。

public  # private だと . で呼び出せない
def foo
  "method"
end

foo = "variable"

# これは変数
foo #=> "variable"
# これはメソッド呼び出し
self.foo #=> "method"

Rubyのクラス名は定数名、つまり変数の一種なので、同じ名前のクラスとメソッドがあるときは上記と同じ動作になります。

public
def Foo
  "method"
end

class Foo
end

# これはクラス
Foo #=> Foo
# これはメソッド呼び出し
Foo() #=> "method"
# これもメソッド呼び出し
self.Foo #=> "method"

Kernelモジュール には、ArrayとかStringという名前のメソッドがあります。

# Stringクラスじゃなくて、KernalのStringメソッドの呼び出し。
String(1) #=> "1"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

brew upgradeしたらopensslが1.1.1gになってしまいRailsが起動できなくなってしまったとき

参考

Cannot install Ruby versions < 2.4 because of openssl@1.1 dependency

経緯

問題
過程
解決

問題

ruby 2.0.0-p648で動いているプロジェクトの修正をローカルで試すタイミングで動かないことに気づきました。

$ bundle exec rails s
Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
  • brew upgradeでopensslが1.1.1gになってしまっていて、今までopenssl1.0.0で動かしていたプロジェクトが動かなくなってしまっていました。

過程

ちょっと試してこの時点では以下の2点を考えていました。
- ruby 2.0.0-p648openssl 1.1.1gでどうにか再インストールできないか?
- openssl 1.1.1gopenssl 1.0.0にダウングレードできないか?

試したこと

How to install openssl 1.0?

エラーで失敗
$ brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/64555220bfbf4a25598523c2e4d3a232560eaad7/Formula/openssl.rb -f

Homebrewで過去のバージョンを使いたい - 2017-01-08
これもダメでした…

rubyの再インストール

rubyの再インストールでなんとかなるような記事もいくつかあったため試したが以下のエラーでインストールすらできない状態に…

The Ruby openssl extension was not compiled.

解決

この時点ではなんとかopenssl 1.1ruby 2.0.0をbuildできないか?と考え、
openssl 1.1 ruby 2.0.0 buildでGoogle検索したところ以下の記事を発見し
Cannot install Ruby versions < 2.4 because of openssl@1.1 dependency
結果的に、openssl 1.0をインストールすることで解決にたどり着きました。

openssl1.0のインストール

rbenv/tapのリポジトリからopenssl1.0をインストールすることで解決しました。

openssl1.0のインストール
$ brew install rbenv/tap/openssl@1.0
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 3 taps (homebrew/core, homebrew/cask and caskroom/cask).
==> Updated Formulae
Updated 13 formulae.
==> Updated Casks
inkscape                                                            inkscape

==> Tapping rbenv/tap
Cloning into '/usr/local/Homebrew/Library/Taps/rbenv/homebrew-tap'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 5 (delta 0), pack-reused 0
Unpacking objects: 100% (5/5), done.
Tapped 1 formula (29 files, 28.6KB).
==> Installing openssl@1.0 from rbenv/tap
==> Downloading https://www.openssl.org/source/openssl-1.0.2t.tar.gz
Warning: Your Xcode (11.0) is outdated.
Please update to Xcode 11.3.1 (or delete it).
Xcode can be updated from the App Store.

==> perl ./Configure --prefix=/usr/local/Cellar/openssl@1.0/1.0.2t --openssldir=/usr/local/etc/openssl no-ssl2 no-ssl3 no-zlib shared e
==> make depend
==> make
==> make test
==> make install MANDIR=/usr/local/Cellar/openssl@1.0/1.0.2t/share/man MANSUFFIX=ssl
==> Caveats
A CA file has been bootstrapped using certificates from the SystemRoots
keychain. To add additional certificates (e.g. the certificates added in
the System keychain), place .pem files in
  /usr/local/etc/openssl/certs

and run
  /usr/local/opt/openssl@1.0/bin/c_rehash

openssl@1.0 is keg-only, which means it was not symlinked into /usr/local,
because Apple has deprecated use of OpenSSL in favor of its own TLS and crypto libraries.

If you need to have openssl@1.0 first in your PATH run:
  echo 'export PATH="/usr/local/opt/openssl@1.0/bin:$PATH"' >> ~/.bash_profile

For compilers to find openssl@1.0 you may need to set:
  export LDFLAGS="-L/usr/local/opt/openssl@1.0/lib"
  export CPPFLAGS="-I/usr/local/opt/openssl@1.0/include"

For pkg-config to find openssl@1.0 you may need to set:
  export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.0/lib/pkgconfig"

==> Summary
?  /usr/local/Cellar/openssl@1.0/1.0.2t: 1,787 files, 12.0MB, built in 8 minutes 8 seconds

改めてrubyインストール

opensslのシンボリックリンクが1.1.1gを指しているので

openssl -> ../Cellar/openssl@1.1/1.1.1g

RUBY_CONFIGURE_OPTSopenssl@1.0に変更してインストール

$ RUBY_CONFIGURE_OPTS="--with-openssl-dir=/usr/local/opt/openssl@1.0" rbenv install 2.0.0-p648 
Downloading ruby-2.0.0-p648.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.0/ruby-2.0.0-p648.tar.bz2
Installing ruby-2.0.0-p648...

WARNING: ruby-2.0.0-p648 is past its end of life and is now unsupported.
It no longer receives bug fixes or critical security updates.

ruby-build: use readline from homebrew
Installed ruby-2.0.0-p648 to ~/.rbenv/versions/2.0.0-p648

rbenv: cannot rehash: ~/.rbenv/shims/.rbenv-shim exists

そのあと

bundlerを追加したり…

ruby2.0.0-p648だと最新版はエラー
$ gem install bundler
Fetching: bundler-2.1.4.gem (100%)
ERROR:  Error installing bundler:
        bundler requires Ruby version >= 2.3.0.
バージョン指定してインストール
$ gem install bundler -v "1.17.2"

プロジェクトのgemを再インストールしたり…

opensslの参照が前のままなのが原因でエラー
$ bundle exec rails s
...
~/vendor/bundle/ruby/2.0.0/gems/activesupport-4.0.0/lib/active_support/dependencies.rb:228:in `require': dlopen(~/vendor/bundle/ruby/2.0.0/gems/mysql2-0.3.20/lib/mysql2/mysql2.bundle, 9): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib (LoadError)
...
.bundle/config
---
BUNDLE_PATH: "vendor/bundle"
BUNDLE_BUILD__LIBV8: "--with-system-v8"
BUNDLE_BUILD__THERUBYRACER: "--with-v8-dir=/usr/local/opt/v8@3.15"
- BUNDLE_BUILD__MYSQL2: "--with-ldflags=-L/usr/local/opt/openssl/lib"
+ BUNDLE_BUILD__MYSQL2: "--with-ldflags=-L/usr/local/opt/openssl@1.0/lib"

systemのv8とgemのlibv8の互換性がなくてエラーが出たり…

However, your system version of v8 could not be located. 

Please make sure your system version of v8 that is compatible 
with 3.16.14.15 installed. You may need to use the 
--with-v8-dir option if it is installed in a non-standard location
        from ~/workspace/project/vendor/bundle/ruby/2.0.0/gems/libv8-3.16.14.15/lib/libv8.rb:7:in `configure_makefile'
        from extconf.rb:32:in `<main>'
$ bundle update libv8
...
Fetching libv8 3.16.14.19 (x86_64-darwin-18) (was 3.16.14.15)
Installing libv8 3.16.14.19 (x86_64-darwin-18) (was 3.16.14.15)
...
Fetching therubyracer 0.12.2
Installing therubyracer 0.12.2 with native extensions
...
Bundle updated!

しましたが無事動くようにできました。

反省と今後の課題

  • brewの仕組みを理解していない
    • 公式以外のリポジトリからインストールなど理解していれば先に選択肢として考えられたはず
    • yumとかで使っているんだからわかる範囲のはずだったのに
  • プログラミング言語(今回はインタプリタ型)のbuildの仕組みや動作を理解していない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 6で認証認可入り掲示板APIを構築する #6 show, create実装

Rails 6で認証認可入り掲示板APIを構築する #5 controller, routes実装

showテストの実装

前回のindexに続き、showのテストとcontrollerを実装していきます。
showは、パラメータのidを元にfindして返すという挙動です。

テストは正常にレスポンスが返ってくることと、存在しないIDの時に404が返ってくることあたりでしょうか。

spec/requests/v1/posts_controller.rb
...

+  describe "GET /v1/posts#show" do
+    let(:post) do
+      create(:post, subject: "showテスト")
+    end
+    it "正常レスポンスコードが返ってくる" do
+      get v1_post_url({ id: post.id })
+      expect(response.status).to eq 200
+    end
+    it "subjectが正しく返ってくる" do
+      get v1_post_url({ id: post.id })
+      json = JSON.parse(response.body)
+      expect(json["post"]["subject"]).to eq("showテスト")
+    end
+    it "存在しないidの時に404レスポンスが返ってくる" do
+      get v1_post_url({ id: post.id + 1 })
+      expect(response.status).to eq 404
+    end
+  end
...

説明が漏れていましたが、letは呼び出された時に遅延評価されます。
つまりpostという変数が呼ばれるまでは実行されず、呼ばれた時点で実行されます。

上記例だと、itブロックの中でpost.idと呼ばれた時にcreateされます。
当然、controller未実装なのでテストはコケます。

showの実装

app/controllers/v1/posts_controller.rb
...
     def show
-      # TODO
+      render json: { post: @post }
     end
...

これで追加した3テストのうち2つは通過しますが、残り1個404エラーのテストが通過しません。
RailsでRecordNotFound例外が起き、jsonじゃないレスポンスが返っちゃっていますね。

全てのcontrollerのsuperクラスであるapplication_controller.rbを修正します。

app/controller/application_controller.rb
 # frozen_string_literal: true

 #
 # controllerのsuperクラス
 #
 class ApplicationController < ActionController::API
+  rescue_from ActiveRecord::RecordNotFound, with: :render_404


+  def render_404
+    render status: 404, json: { message: "record not found." }
+   end
 end

※無駄に2行空いているのはQiitaのシンタックスハイライトがうまく効かないだけなので、本来は1行でOKです

上記のように対応することで、レコードが見つからない(ActiveRecord::RecordNotFound)例外が投げられた時にrescueされ、render_404が実行されます。
そしてstatus: 404の通り404レスポンスコードでjsonが返されます。

ここまで実装するとテストも通過します。
念の為curlでも確認してみましょう。

$ curl localhost:8080/v1/posts/0
{"message":"record not found."}
$ curl localhost:8080/v1/posts/1
{"post":{"id":1,"subject":"hoge","body":"fuga","created_at":"2020-09-05T13:50:01.797Z","updated_at":"2020-09-05T13:50:01.797Z"}}

createテストの実装

createは新しいレコードを生成するので、1レコード増えること、増えたレコードが登録時と一致すること、不正なパラメータの時に生成できないことを確認します。

spec/requests/v1/posts_controller.rb
+  describe "POST /v1/posts#create" do
+    let(:new_post) do
+      attributes_for(:post, subject: "create_subjectテスト", body: "create_bodyテスト")
+    end
+    it "正常レスポンスコードが返ってくる" do
+      post v1_posts_url, params: new_post
+      expect(response.status).to eq 200
+    end
+    it "1件増えて返ってくる" do
+      expect do
+        post v1_posts_url, params: new_post
+      end.to change { Post.count }.by(1)
+    end
+    it "subject, bodyが正しく返ってくる" do
+      post v1_posts_url, params: new_post
+      json = JSON.parse(response.body)
+      expect(json["post"]["subject"]).to eq("create_subjectテスト")
+      expect(json["post"]["body"]).to eq("create_bodyテスト")
+    end
+    it "不正パラメータの時にerrorsが返ってくる" do
+      post v1_posts_url, params: {}
+      json = JSON.parse(response.body)
+      expect(json.key?("errors")).to be true
+    end
+  end

attributes_forを使うとbuildと同じくDBにsaveしないでオブジェクトが返ってきます。
その際にActiveRecord形式ではなくシンプルなオブジェクトとして返ってくるかつ、idやcreated_at, updated_atというpost時に不要なカラムは存在しないので便利です。

なお、この際に変数名をpostにしないようご注意ください。
筆者は記事執筆当時postにしてしまい、post v1_posts_url, params: postとなりget, postのpostがオーバーライドされてテストが正常に動かない状態となりました…解消までかなり時間がかかりました:sweat_smile:

expect do
post v1_posts_url, params: new_post
end.to change { Post.count }.by(1)

このブロックはpostをすることでPost.countが1増加するテストとなります。expect.to eqではなくexpectがブロックになっているのがポイントです。

createの実装

controllerを実装します。

app/controllers/v1/posts_controller.rb
...

     def create
-      # TODO
+      post = Post.new(post_params)
+      if post.save
+        render json: { post: post }
+      else
+        render json: { errors: post.errors }
+      end
    end
...
$ curl localhost:8080/v1/posts -X POST -H 'Content-Type: application/json' -d '{"subject":"moge","body":"hoge"}'        
{"post":{"id":3,"subject":"moge","body":"hoge","created_at":"2020-09-06T10:31:38.375Z","updated_at":"2020-09-06T10:31:38.375Z"}}

rubocopとrspecが正常に動いたらcommit。

本記事においてとても参考にさせていただきました。ありがとうございます。
参考:Railsで超簡単API

続き

Rails 6で認証認可入り掲示板APIを構築する #7 update, destroy実装

連載目次へ

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

Rails アプリケーションの作成準備

アプリケーションの作成

command
% rails _6.0.0_ new application -d mysql

・「6.0.0」でバージョンを指定しています。
・「application」は作成したいアプリケーション名を示します。
・「- d mysql」のオプションをつけることで、
  データ管理ツールとしてMYSQLを使用します。

データベースの作成

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock

「utf8」のようにエンコーディングの設定を行うことができます。

command
% rails db:create 

railsコマンドでデータベースを作成。

ルーティングの設定

config/route.rb
Rails.application.routes.draw do
  root to: "notes#index"
end

ルートパスへのアクセスがあったら、notes_controllerのindexアクションが呼び出されるようになる。

ビューとコントローラーの設定

config/application.rb
# 省略
 config.load_defaults 6.0
# 中略
 config.generators do |g|
   g.stylesheets false
   g.javascripts false
   g.helper false
   g.test_framework false
 end

rails gコマンドでコントローラーを作成する前に、必要のないファイルを生成しないように設定。

commnad
% rails g controller notes index

コントローラー作成時にコントローラ名に続けてアクション名を指定すると、
・notesコントローラーにindexアクションが作られる
・viewsのnotesフォルダにindex.html.erbファイルが作られる

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

privateメソッド

はじめに、前投稿でゲッターで得た値をインスタンス名.ゲッターメソッド名で出力可能ですと記述いたしましたが、
誤りでした、申し訳ありません。インスタンスの値が代入された、変数を用いる場合、変数.ゲッターメソッド名で出力致します。

以下、プライベートメソッドについての投稿です。

また、今回投稿するに当たって参考にさせて頂いたURLです。
ありがとうございました。
https://26gram.com/private-protected-in-ruby
https://qiita.com/kidach1/items/055021ce42fe2a49fd66

プライベートメソッドとはprivate以下の記述をクラス外から呼び出すことが出来ないというものです。

class Fruits

  private

  def name
    puts "りんご"
  end

end

apple = Fruits.new
apple.name
出力結果
 private method `name' called for #<Fruits:0x00007fe767832538> (NoMethodError)

この記述ですと、インスタンスメソッドがprivateより以下に記述されていますので、出力できません。
エラーが生じます。クラス外から、呼び出すことが出来ないからです。

また、次のような記述もエラーが生じます。

class Fruits

  apple.name 

  private

  def name
    puts "りんご"
  end

end

apple = Fruits.new
出力結果
undefined local variable or method `apple' for Fruits:Class (NameError)

では、どうすることが適切なのでしょうか。

private以上に以下の記述を呼び出すメソッドを定義しまして、
そのメソッドをクラス外から呼び出します。

class Fruits

  def info
    name
  end

  private

  def name
    puts "りんご"
  end

end

apple = Fruits.new
apple.info
出力結果
りんご

また、親クラスのプライベートメソッドは子クラスで呼び出すことも可能です。

class InfoFruits
  private

  def name
    puts "りんご"
  end

end

class Fruits < InfoFruits

  def info
    name
  end

end

apple = Fruits.new
apple.info
出力結果
りんご

最後に、親クラスのprivateメソッドであるゲッターを利用しまして、
その値を出力しています。

class InfoFruits

  private

  def name #ゲッター
    @name
  end

end

class Fruits < InfoFruits #実態のあるインスタンスは継承できませんが、定義されたインスタンスメソッドとインスタンス変数は用いることができます。

  def fruit_name(info_fruit_name)
    @name = info_fruit_name #インスタンス変数の定義
  end

  def info #プライベートメソッドnameを呼び出しています。
    name
  end

  def info_fruit
    puts "#{name}" #前投稿で**インスタンス名.ゲッターメソッド名**で出力可能と記述いたしましたが、
                   #誤りでした、すみません。インスタンスの値が代入された、変数を用いる場合は**変数.ゲッターメソッド名**で出力致します。
                   #今回、変数を用いていませんので、この形で出力が可能となります。
  end

end

apple = Fruits.new #インスタンスの生成
apple.info #プライベートメソッドを呼び出す為のメソッドを呼び出しています。
apple.fruit_name("りんご") #fruit_nameメソッドに引数を渡しています。

apple.info_fruit #info_fruitメソッドを呼び出しています。

出力結果
りんご

以上となります。

誤っている箇所、認識が不足している部分について、ご指摘頂ければ幸いに存じます。

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

HerokuでDocker環境Rails MySQLなものを動かす。deviseかつtwitter APIを隠しながら

人に見せる用に書いていません。

自作記事のお父上、Rails+MySQL+Nginx+Unicorn+Docker+CircieCIな開発環境を作りたいえ〜の記事の続き。

上で作った環境に[Rails] deviseの使い方(rails5版)を追加し、かつTwitterAPIをgem 'dotenv-rails'で隠しながら、本番環境のherokuにアップロードした。

①docker-compose.ymlのコマンドを変更する

docker-compose.yml
command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"

② gem 'dotenv-rails'を導入する

目的:TwitterAPIキーをGitHubのローカルに入れたくない

envファイルはGitの管理に入れたくないので、.gitignoreファイルに以下を追記しておきましょう。

.gitignore.envを追加する。

これで.envはGitの管理下から外れます。

https://pikawaka.com/rails/dotenv-rails
http://vdeep.net/rubyonrails-dotenv

③ APIキーをdotenv-railsの.envで隠しながら導入する

[Rails] deviseの使い方(rails5版)の記事のtwitterAPIを追加する場所をENV[]に書き換える

config/initializer/devise.rb
config.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['DATABASE_URL']

④ Herokuの環境変数を変更する

多分、DATABASE_URLとTWIITER_API_KEY,DATABASE_URLはマスト。

herokuのGUIのSettingのConfig VarsHerokuアプリの環境変数設定(CUIで設定)で設定する

Herokuアプリの環境変数設定

  • ClearDBのURL確認
    以下のコマンドで、ClearDBのURLが確認できる。
$ heroku config
=== <アプリの名前> Config Vars
CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
  • 環境変数の設定
    上記コマンドで表示されたそれぞれの値を変数に設定する。
$ heroku config:add DB_NAME='<データベース名>'
$ heroku config:add DB_USERNAME='<ユーザー名>'
$ heroku config:add DB_PASSWORD='<パスワード>'
$ heroku config:add DB_HOSTNAME='<ホスト名>'
$ heroku config:add DB_PORT='3306'
$ heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'

※ RailsのGemfileで'mysql2'を使用しているので、DATABASE_URLmysql2://で始める必要がある。

設定内容を確認すると、以下のように表示される。

$ heroku config
=== <アプリの名前> Config Vars
CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
DATABASE_URL:         mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
DB_HOSTNAME:          <ホスト名>
DB_NAME:              <データベース名>
DB_PASSWORD:          <パスワード>
DB_PORT:              3306
DB_USERNAME:          <ユーザー名>

Herokuへデプロイ

Railsプロジェクトに必要な準備

  • config/environments/production.rbに以下を追記
    (この辺りの設定に関しては、後々修正します。)
config.assets.compile = true
config.assets.initialize_on_precompile=false

これプラス、gem 'dotenv-rails'.envで作ったTwitterAPIをheroku側に設定する。

参考
https://qiita.com/ymstshinichiro/items/d6ea229f6eb4778006c2
https://golikyua.hatenablog.com/entry/2020/01/09/135254
https://www.sejuku.net/blog/tutorial/111347

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

簡易LISP処理系の実装例(Ruby版)

【2020-09-11追記】JavaScript版の記事を作成しました.

Python版Scheme版C言語版に続く第四の実装例です.そろそろ,まとめリンク集の記事を作るかも.

この記事は,下記拙作記事のRuby版を抜粋・修正したものを利用した,簡易LISP処理系の実装例をまとめたものです.

主旨およびプログラムコードはPython版とほぼ同じで,最低限の機能をもったLISP処理系の実装の場合,本体である超循環評価器(meta-circular evaluator)の作成はとても簡単であり,むしろ,字句・構文解析を行うS式入出力やリスト処理実装の方が開発言語ごとの手間が多く,それが敷居になっている人向けにまとめています.

処理系の概要

実行例は次の通り.Ruby 2.5.5にて確認.

>> s_rep("(car (cdr '(10 20 30)))")
=> "20"
>> s_rep(gets.chomp)
((lambda (x) (car (cdr x))) '(abc def ghi))
=> "def"
>> s_rep("((lambda (f x y) (f x (f y '()))) 'cons '10 '20)")
=> "(10 20)"
>> s_rep("((lambda (f x y) (f x (f y '())))
          '(lambda (x y) (cons x (cons y '())))
          '10 '20)")
=> "(10 (20 ()))"
>> s_rep("((lambda (assoc k v) (assoc k v))
          '(lambda (k v)
             (cond ((eq v '()) nil)
                   ((eq (car (car v)) k)
                    (car v))
                   ('t (assoc k (cdr v)))))
          'Orange
          '((Apple . 120) (Orange . 210) (Lemmon . 180)))")
=> "(Orange . 210)"

実装内容は次の通り.

  • "McCarthy's Original Lisp"をベースにした超循環評価器
  • 数字を含むアトムは全てシンボルとし,変数の値とする場合はquote')を使用
  • 構文としてquoteの他,condlambdaが使用可能
  • 組込関数:atom eq cons car cdr(内部でコンスセルを作成)
  • 真偽値はt(真)およびnil(偽)=空リスト
  • エラーチェックなし,モジュール化なし,ガーベジコレクションなし

"McCarthy's Original Lisp"の詳細についてはScheme版記事を参照.ダイナミックスコープということもあり,実行例ではlambda式をletrec(Scheme)やlabels(Common Lisp)などの代わりに使用しています.

実装例

ソースコード一式

jmclisp.rb
####
#### JMC Lisp: defined in McCarthy's 1960 paper,
#### with S-expression input/output and basic list processing
####


#### basic list processing: cons, car, cdr, eq, atom
def cons(x, y) [x, y].freeze end
def car(s) s[0] end
def cdr(s) s[1] end
def eq(s1, s2) s1 == s2 end
def atom(s) s.is_a?(String) || eq(s, nil) || eq(s, true) || eq(s, false) end


#### S-expression input: s_read

def s_lex(s)
  for p in ['(',')','\''] do
    s = s.gsub(p, ' ' + p + ' ')
  end
  s.split
end

def s_syn(s)
  def quote(x, s)
    if s.length != 0 && s[-1] == '\'' then
      s.delete_at(-1)
      cons("quote", cons(x, nil))
    else x
    end
  end
  t = s.delete_at(-1)
  if t == ')' then
    r = nil
    while s[-1] != '(' do
      if s[-1] == '.' then
        s.delete_at(-1)
        r = cons(s_syn(s), car(r))
      else
        r = cons(s_syn(s), r)
      end
    end
    s.delete_at(-1)
    quote(r, s)
  else
    quote(t, s)
  end
end

def s_read(s) s_syn(s_lex(s)) end


#### S-expression output: s_string

def s_strcons(s)
  sa_r = s_string(car(s))
  sd = cdr(s)
  if eq(sd, nil) then sa_r
  elsif atom(sd) then sa_r + " . " + sd
  else sa_r + " " + s_strcons(sd)
  end
end

def s_string(s)
  if    eq(s, nil)   then "()"
  elsif eq(s, true)  then "t"
  elsif eq(s, false) then "nil"
  elsif atom(s) then s
  else "(" + s_strcons(s) + ")"
  end
end


#### JMC Lisp meta-circular evaluator: s_eval

def caar(x) car(car(x)) end
def cadr(x) car(cdr(x)) end
def cadar(x) car(cdr(car(x))) end
def caddr(x) car(cdr(cdr(x))) end
def caddar(x) car(cdr(cdr(car(x)))) end

def s_null(x) eq(x, nil) end

def s_append(x, y)
  if s_null(x) then y
  else cons(car(x), s_append(cdr(x), y))
  end
end

def s_list(x, y) cons(x, cons(y, nil)) end

def s_pair(x, y)
  if s_null(x) and s_null(y) then nil
  elsif (not atom(x)) and (not atom(y))
    cons(s_list(car(x), car(y)), s_pair(cdr(x), cdr(y)))
  else nil
  end
end

def s_assoc(x, y)
  if eq(caar(y), x) then cadar(y)
  else s_assoc(x, cdr(y))
  end
end

def s_eval(e, a)
  if    eq(e, "t")   then true
  elsif eq(e, "nil") then false
  elsif atom(e) then s_assoc(e, a)
  elsif atom(car(e))
    if    eq(car(e), "quote") then cadr(e)
    elsif eq(car(e), "atom")  then atom(s_eval(cadr(e), a))
    elsif eq(car(e), "eq")    then eq(  s_eval(cadr(e), a), s_eval(caddr(e), a))
    elsif eq(car(e), "car")   then car( s_eval(cadr(e), a))
    elsif eq(car(e), "cdr")   then cdr( s_eval(cadr(e), a))
    elsif eq(car(e), "cons")  then cons(s_eval(cadr(e), a), s_eval(caddr(e), a))
    elsif eq(car(e), "cond")  then evcon(cdr(e), a)
    else s_eval(cons(s_assoc(car(e), a), cdr(e)), a)
    end
  elsif eq(caar(e), "lambda")
    s_eval(caddar(e), s_append(s_pair(cadar(e), evlis(cdr(e), a)), a))
  else print("Error")
  end
end

def evcon(c, a)
  if s_eval(caar(c), a) then s_eval(cadar(c), a)
  else evcon(cdr(c), a)
  end
end

def evlis(m, a)
  if s_null(m) then nil
  else cons(s_eval(car(m), a), evlis(cdr(m), a))
  end
end


#### REP (no Loop): s_rep
def s_rep(e) s_string(s_eval(s_read(e), s_read("()"))) end

解説

  • リスト処理:cons car cdr eq atom
    先の記事より,ほぼそのまま抜粋.

  • S式入力:s_read
    Python版とほぼ同じ.先の記事から,字句解析部を()および'の識別に変更(s_lex),抽象構文木生成部をドット対とクォート記号対応としつつ,リスト処理関数でコンスセルによる構文木を生成するよう変更(s_syn),それらをまとめたS式入力関数s_readを定義.

  • S式出力:s_string
    ほぼPython版の書き換え.内部ではnilである空リストは()を,真偽値はt nilを出力するよう設定.

  • 超循環評価器:s_eval+ユーティリティ関数
    こちらも,ほぼPython版の書き換え."McCarthy's Original Lisp"をベースにs_eval関数およびユーティリティ関数を作成.

  • REP (no Loop):s_rep
    s_reads_evals_stringをまとめたs_repを定義.

備考

記事に関する補足

  • 超循環評価器のみの場合,約70行/1950バイトほど.Python版と比較してthenendの分だけが増えている感じ?コメントの御意見を受けて,ほぼ不要のreturnと一部のthenを削除したところ,約60行/1560バイトほど.むしろPython版よりも小さくなったという.

更新履歴

  • 2020-09-11:ソースコードから不要なreturn等を削除(コメントより)
  • 2020-09-11:実装例をソースコード一式+解説の構成に変更
  • 2020-09-11:初版公開
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails チュートリアル

次回2.2.2から

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