- 投稿日:2020-09-11T23:22:14+09:00
【RubyonRails】APIでadd_token_to_usersしてログイン機能を実装する
はじめに
APIでウェブサービスを制作中。
ユーザーのログイン機能を実装することに。
APIのログイン機能作るのは初めてだったので、下記のページを参考に実装。Railsでトークン認証のログインAPI実装 - Qiita
環境
MacBookAir
ruby 2.6.3
Rails 6.0.3.2ルーティングを考える
config/routes.rbRails.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.rbclass 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.rbmodule 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 endPOSTMANでログインを試してみる
できたっぽい。
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)終わりに
これからログアウト機能も実装しないと。
- 投稿日:2020-09-11T23:05:00+09:00
ユーザー管理機能をウィザード形式で実装する
概要
今回は、ユーザー管理機能をウィザード形式で実装することに関してです。
ウィザード形式とは、画面遷移しながらユーザー登録を行っていく形式です。今回、userモデルの登録要件は
nickname, grade, email, password続いて画面遷移してuser_infoモデルに
subject, school, profileを登録します。
ちなみに、user_infoは得意教科、在籍校、プロフィールとなっています。
実装
まずはdeviseをインストールして、userモデルを作成します。
emailとpasswordはデフォルトで入っているので、nicknameとgradeを追加します。
カラムを追加したので、次のようにコントローラーを編集します。
controllers.application_controller.rbclass 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.rbRails.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.rbdef 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 endsessionを用いて1ページ目に記述したデータを持ってきます。
そしてattributesメソッドでデータを整形しています。build_user_infoで今回生成したインスタンス@userに紐づくUserInfoモデルのインスタンスを生成します。ここで生成したUserInfoモデルのインスタンスは、@user_infoというインスタンス変数に代入します。そして、住所情報を登録させるページを表示するnew_user_infoアクションのビューへrenderします。
user_infoを登録させるページを表示するnew_user_infoアクションと住所情報を登録するcreate_user_infoアクションのルーティングを設定しましょう。
routes.rbRails.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.rbdef 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) enduserをsaveした後に、.clearでsessionを削除しています。
そして、保存された後にログインされるように記述されています。最後に、create_user_infoアクションに対応するビューを作成します。
views/devise/registrations/create_user_info.html.erb<h2>登録が完了しました</h2> <%= link_to "トップへ戻る", root_path%>以上です。
感想
初めてウィザード形式の登録機能を実装しました。
本当はgradeの学年のところをactive_hashで実装したかったのですが、二時間エラーに悩まされた末に断念しました、、、
一通り実装してみて余裕があったらまたチャレンジしてみます。
日々頑張ってはいるのですが、あまり成長を感じられないです。
めちゃくちゃ記事みないと何もできません、、、頑張ります。
- 投稿日:2020-09-11T22:57:19+09:00
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.789Nginxのインストール
以下のコマンドを実行し、Nginxをインストールする。
sudo yum -y install nginxNginxの設定ファイルを編集する
以下のコマンドを実行しvimを使ってNginxの設定ファイルを編集する。
/etcディレクトリ以下のファイルは権限がないと読み書き保存ができないため、sudoで実行する。sudo vim /etc/nginx/conf.d/rails.conf以下のようにrails.confを編集する。
今回はアプリケーション名が「testapp」、Elastic IPが「123.456.789」の場合を例として進める。rails.confupstream 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/libsudo chmod -R 775 nginx「-R」オプション
「-R」オプションは再帰的に変更するためのオプションである。
つまり、そのディレクトリとディレクトリ内の全てのファイルの権限を変更する。POSTメソッドとは
HTTP通信でクライアントからWebサーバに送るリクエストの一つで、URLで指定したプログラムなどに対してクライアントからデータを送信するためのもの。大きなデータやファイルをサーバに送るために使われる。
Nginxを再起動する
以下のコマンドを実行し、Nginxを再起動し、設定ファイルを再読み込みする。
cd ~sudo service nginx restartunicornの設定を修正する
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 masterunicornを再起動する
以下のコマンドを実行し、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サーバに返す
- 投稿日:2020-09-11T22:25:55+09:00
セレクトボックスから複数のデータを取り出すまで
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.rbclass User < ApplicationRecord has_secure_password has_many :languages, through: :user_language has_many :user_language def language_name ::LanguageSelect::LANGUAGES[language] end endLanguageモデル
models/language.rbclass Language < ApplicationRecord has_many :users, through: :user_language has_many :user_language endUserモデルとLanguageモデルの中間テーブル
models/user_language.rbclass Language < ApplicationRecord belongs_to :user belongs_to :language endschema
db/schema.rbActiveRecord::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.rbdef 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.rbclass User < ApplicationRecord has_secure_password has_many :user_language has_many :languages, through: :user_language def language_name ::LanguageSelect::LANGUAGES[language] end endLanguageモデル
models/language.rbclass Language < ApplicationRecord has_many :user_language has_many :users, through: :user_language endAssociationTypeMissmatchのエラー
書き直す事で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.rbdef 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.rbdef 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つともどのように記述していけば良いか分からず現在試行錯誤している状況です。
解決策を期待して読んで下さった方には申し訳ありませんが、進捗が有り次第更新していきます。
- 投稿日:2020-09-11T18:54:38+09:00
errors.full_messagesの配列を文字に変えて出力する
起こった問題とやりたいこと
errors.full_messagesが配列なので、flashメッセージが、["hoge hoge"]が出力される
→ 文字に変えて、hogehogeと出力したいやったこと
errors.full_messagesにto_sentenceメソッドをつけるだけ
user_contorller.rbclass Users::UserController < Users::ApplicationController #中略 def update @user = user.Users flash[:alert] = @user.errors.full_messages.to_sentence end #中略 end今日もお疲れ様でした。
- 投稿日:2020-09-11T18:05:06+09:00
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をインストール
解決できました。
- 投稿日:2020-09-11T16:12:46+09:00
【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
が返される- メソッドが存在しない(スペルミス)場合はエラーが出力されてしまう
という感じでしょうか。ご指摘等ありましたら、よろしくお願いします?♂️
- 投稿日:2020-09-11T13:24:54+09:00
【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:statuscontroller
【作成】shopはディレクトリを分けている場合使用。rails g controller shop::posts new【削除】rails destroy controller shop::posts開発環境
ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina
- 投稿日:2020-09-11T13:07:50+09:00
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は文献が少ないので積極的にアウトプットしていこう
- 投稿日:2020-09-11T11:01:24+09:00
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"
- 投稿日:2020-09-11T10:53:57+09:00
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-p648
をopenssl 1.1.1g
でどうにか再インストールできないか?
-openssl 1.1.1g
をopenssl 1.0.0
にダウングレードできないか?試したこと
エラーで失敗$ brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/64555220bfbf4a25598523c2e4d3a232560eaad7/Formula/openssl.rb -fHomebrewで過去のバージョンを使いたい - 2017-01-08
これもダメでした…rubyの再インストール
rubyの再インストールでなんとかなるような記事もいくつかあったため試したが以下のエラーでインストールすらできない状態に…
The Ruby openssl extension was not compiled.解決
この時点ではなんとか
openssl 1.1
でruby 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_OPTS
でopenssl@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の仕組みや動作を理解していない
- 投稿日:2020-09-11T10:04:26+09:00
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がオーバーライドされてテストが正常に動かない状態となりました…解消までかなり時間がかかりました
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実装
【連載目次へ】
- 投稿日:2020-09-11T08:56:05+09:00
Rails アプリケーションの作成準備
アプリケーションの作成
command% rails _6.0.0_ new application -d mysql・「6.0.0」でバージョンを指定しています。
・「application」は作成したいアプリケーション名を示します。
・「- d mysql」のオプションをつけることで、
データ管理ツールとしてMYSQLを使用します。
データベースの作成
config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: socket: /tmp/mysql.sock「utf8」のようにエンコーディングの設定を行うことができます。
command% rails db:createrailsコマンドでデータベースを作成。
ルーティングの設定
config/route.rbRails.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 endrails gコマンドでコントローラーを作成する前に、必要のないファイルを生成しないように設定。
commnad% rails g controller notes indexコントローラー作成時にコントローラ名に続けてアクション名を指定すると、
・notesコントローラーにindexアクションが作られる
・viewsのnotesフォルダにindex.html.erbファイルが作られる
- 投稿日:2020-09-11T08:28:03+09:00
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メソッドを呼び出しています。出力結果 りんご以上となります。
誤っている箇所、認識が不足している部分について、ご指摘頂ければ幸いに存じます。
- 投稿日:2020-09-11T02:56:53+09:00
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.ymlcommand: /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.rbconfig.omniauth :twitter, ENV['TWITTER_API_KEY'], ENV['DATABASE_URL']④ Herokuの環境変数を変更する
多分、DATABASE_URLとTWIITER_API_KEY,DATABASE_URLはマスト。
herokuのGUIのSettingのConfig VarsかHerokuアプリの環境変数設定(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_URL
はmysql2://
で始める必要がある。
設定内容を確認すると、以下のように表示される。$ 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
- 投稿日:2020-09-11T02:24:52+09:00
簡易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
の他,cond
とlambda
が使用可能- 組込関数:
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_read
→s_eval
→s_string
をまとめたs_rep
を定義.備考
記事に関する補足
- 超循環評価器のみの場合,
約70行/1950バイトほど.Python版と比較してコメントの御意見を受けて,ほぼ不要のthen
とend
の分だけが増えている感じ?return
と一部のthen
を削除したところ,約60行/1560バイトほど.むしろPython版よりも小さくなったという.更新履歴
- 2020-09-11:ソースコードから不要な
return
等を削除(コメントより)- 2020-09-11:実装例をソースコード一式+解説の構成に変更
- 2020-09-11:初版公開