- 投稿日:2020-09-21T23:17:22+09:00
Ruby×Sinatraで作ったSlackBotをAWS Lambdaで動かしてみる
本記事で目差す構成
① Slackで特定のアクションを実行する。(※今回はスラッシュコマンド)
② API Gatewayを介してLambdaを起動。
③ Lambdaに配置した関数を実行し情報を返す。↑動作イメージとしてはこんな感じ。
今回はとある地域の現在気温を返してくれるSlackBotを動かしてみる。対象読者
- 簡単なSlackBotを作ってみたい人
- AWSのLambdaに触れてみたい人
Lambdaとは?
AWS Lambda はサーバーをプロビジョニングしたり管理する必要なくコードを実行できるコンピューティングサービスです。 AWS Lambda は必要時にのみてコードを実行し、1 日あたり数個のリクエストから 1 秒あたり数千のリクエストまで自動的にスケーリングします。使用したコンピューティング時間に対してのみお支払いいただきます- コードが実行中でなければ料金はかかりません。AWS Lambda では、管理を全く必要とせずに、任意のアプリケーションやバックエンドサービスで仮想的にコードを実行できます。AWS Lambda は、高度な可用性のコンピューティングインフラストラクチャでコードを実行し、サーバーとオペレーティングシステム、システムのメンテナンス、容量のプロビショニングと自動スケーリング、コードのモニタリングやログ記録など、コンピューティングリソースのすべての管理を実行します。必要な操作は、AWS Lambda がサポートするいずれかの言語でコードを指定するだけです。(引用: Amazon公式ドキュメント)
これだけだとイマイチわかりにくいが、要するに「プログラムを実行するためのサーバーがいらない」という事。
通常、何かしらのプログラムを実行しようと思った場合、サーバーを購入したり、各種ミドルウェアをインストールしたりと色々手間がかかるものだが、Lambdaにおいてそういったものは全てAWSが管理してくれるため、開発者はソースコードの作成にだけ力を注げば良くなるらしい。
Lambdaを使うメリット
- サーバーや各種ミドルウェアの管理が不要
- 上述の理由から。
- コスト削減
- リクエスト数やプログラムの実行時間によって課金される仕組みとなっており、待機時間には課金されないため、使用する局面によっては大幅なコストダウンが可能。(常に課金され続けるEC2とは対照的)
- オートスケーリング
- アクセス数や負荷に応じて自動的にサーバーの数を増減してくれる。
今回のようなSlackBotの場合、常に稼働させたいというよりは必要な時のみ動いてくれれば構わないため、Lambdaを利用するにはちょうど良いと思った。
SlackBotくらい軽い実装であればHerokuなどを使ったデプロイ方法も定番だが、今時のAWSを使ってみたい感がある。
仕様
言語: Ruby2.5
フレームワーク: Sinatra
インフラ: AWS LambdaSlackBotを作成
まず、肝心のSlackBotを作成していく。
ディレクトリを作成
$ mkdir slack-bot-on-aws-lambda $ cd slack-bot-on-aws-lambdaRubyのバージョンを指定
# 2.5系なら何でもOK $ rbenv local 2.5.1Sinatraをインストール
$ bundle init↑のコマンドでGemfileを生成し、以下のように編集する。
./Gemfile# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem 'sinatra'その後、Gemをインストール。
$ bundle install --path vendor/bundle動作確認のため、とりあえず「Hello World!」と返すページを実装してみる。
$ touch main.rb./main.rbrequire 'sinatra' get '/' do 'Hello World!' endその後、Sinatraを起動。
$ bundle exec ruby main.rb [2020-09-21 20:47:35] INFO WEBrick 1.4.2 [2020-09-21 20:47:35] INFO ruby 2.5.1 (2018-03-29) [x86_64-darwin19] == Sinatra (v2.1.0) has taken the stage on 4567 for development with backup from WEBrick [2020-09-21 20:47:35] INFO WEBrick::HTTPServer#start: pid=50418 port=4567Sinatraはデフォルトだとポート番号「4567」で動くため、「localhost:4567」にアクセス。
「Hello World!」と表示されれば成功。
天気情報を返すプログラムを実装
今回作るSlackBotの主な機能である天気情報を返すプログラムを実装していく。
OpenWeatherのAPIキーを取得
上記サイトに会員登録し、APIキーを取得。
英語で書かれたサービスだが、ある程度は直感的に操作できるので詳しい説明は省略。どうしてもわからなかったらググればいくらでも記事が出てくるはず。
各種Gemをインストール
この先の処理を行う上で必要なGemがいくつかあるため、このタイミングで一気にインストールしておく。
./Gemfilegem 'faraday' gem 'rack' gem 'rack-contrib' gem 'rubysl-base64' gem 'slack-ruby-bot'「bundle install」も忘れずに。
$ bundle install --path vendor/bundlesrc/weather.rbを作成
$ mkdir src $ touch src/weather.rbsrc/weather.rbを作成し、次のように記述。
./src/weather.rbrequire 'json' class Weather def current_temp(locate) end_point_url = 'http://api.openweathermap.org/data/2.5/weather' api_key = # 先ほど取得したOpenWeatherのAPIキー res = Faraday.get(end_point_url + "?q=#{locate},jp&APPID=#{api_key}") res_body = JSON.parse(res.body) temp = res_body['main']['temp'] celsius = temp - 273.15 celsius_round = celsius.round return "現在の練馬の気温は#{celsius_round.to_s}℃です。" end endmain.rbを編集
./main.rbrequire 'slack-ruby-client' require 'sinatra' require './src/weather' Slack.configure do |conf| conf.token = # SlackBotのトークン end get '/' do 'This is SlackBot on AWS Lambda' end post '/webhook' do client = Slack::Web::Client.new channel_id = params['channel_id'] command = params['command'] case command when '/nerima' # スラッシュコマンド「/nerima」が実行された場合に以下の処理が走る。 weather = Weather.new client.chat_postMessage channel: channel_id, text: weather.current_temp('Nerima'), as_user: true # 'Nerima'の部分は各自変更してOK。「Shinjuku」に変えれば新宿の気温を返すはず。 end return endSlackBotのトークンを取得する方法については次の記事を参照。
参照: ワークスペースで利用するボットの作成
参照: API トークンの生成と再生成実際に動作確認
SlackBotをスラッシュコマンドで呼び出すためにいくつか設定しなければならない事がある。
https://api.slack.com/apps/
↑のURLにアクセスし、該当のBotを選択。
左サイドメニューに「Slash Commands」という項目があるので選択し、「Create New Command」をクリック。
各項目を入力していく。
- Command: 任意のスラッシュコマンド。
- 今回は東京度練馬区の現在気温を返す事を想定しているので「/nerima」としているが、たとえば新宿区であれば「/shinjuku」とかでもOK。)
- Request URL: スラッシュコマンドを実行した際にリクエストしたいURL。
- 「localhost」では動かないため、今回はngrokを使って独自のドメインを割り当てている。
- 参照: ngrokの利用方法
- 「bundle exec ruby main.rb」でSinatraを起動した後、別のターミナルで「ngrok http 4567」と叩いて表示されたURLを使用する。
- postメソッドでリクエストしたいので、「https://********.ngrok.io/webhook」と入力する。
- Short Description: スラッシュコマンドの簡単な説明。
入力が完了したら右下の「Save」をクリック。
スラッシュコマンドの作成が終わったら、SlackBotを追加したチャンネルで「/nerima」と打ち込んでみる。上手くいけば画像のようにSlackBotから返答が来る。(設定で画像や名前を変えたりする事も可能。)
何か不具合があった場合はターミナルにログが出力されているはずなので、適宜デバッグ。AWS Lambdaにデプロイ
正常に動作確認できたら、いよいよAWS Lambdaで本番稼働させる。
AWS CLIをインストール
今回は「AWS CLI」と呼ばれるツールを使いながらデプロイしていくので、まだインストールできてないない場合はインストールしておく。
$ brew install awscliIAMユーザーを作成
デプロイ作業を行うためのIAMユーザーを作成していく。
まずは「IAM」→「ポリシー」→「ポリシーの作成」へと進み、JSONタブから以下の文を貼り付ける。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "apigateway:*", "cloudformation:*", "dynamodb:*", "events:*", "iam:*", "lambda:*", "logs:*", "route53:*", "s3:*" ], "Resource": [ "*" ] } ] }
適当にポリシー名や説明を記述し、「ポリシーの作成」をクリック。
次に「IAM」→「ユーザー」→「ユーザーの作成」へと進み、適当な名前を付けた後「プログラムによるアクセス」にチェックを入れて次へ進む。
「既存のポリシーを直接アタッチ」から先ほど作成した「MinimalDeployIAMPolicy」を選択し、次へ進む。(タグは任意でOK)最後に確認画面が表示されるので、問題無ければ「ユーザーの作成」をクリック。
すると「アクセスキーID」と「シークレットアクセスキー」の2つが発行されるので、csvファイルをダウンロードするなりメモするなり大事に保管しておく。AWS CLIの設定
$ aws configure AWS Access Key ID # 先ほど作成したアクセスキーID AWS Secret Access Key # 先ほど作成したシークレットアクセスキー Default region name # ap-northeast-1 Default output format # jsonターミナルで「aws configure」と打ち込むと対話形式で色々聞かれるので、それぞれ必要な情報を入力していく。
各種ファイルを作成
AWS CLIの設定が終わったら、デプロイに必要な各種ファイルの作成を行う。
- config.ru
- lambda.rb
- template.yaml
./config.rurequire 'rack' require 'rack/contrib' require_relative './main' set :root, File.dirname(__FILE__) run Sinatra::Application./lambda.rbrequire 'json' require 'rack' require 'base64' $app ||= Rack::Builder.parse_file("#{__dir__}/config.ru").first ENV['RACK_ENV'] ||= 'production' def handler(event:, context:) body = if event['isBase64Encoded'] Base64.decode64 event['body'] else event['body'] end || '' headers = event.fetch 'headers', {} env = { 'REQUEST_METHOD' => event.fetch('httpMethod'), 'SCRIPT_NAME' => '', 'PATH_INFO' => event.fetch('path', ''), 'QUERY_STRING' => Rack::Utils.build_query(event['queryStringParameters'] || {}), 'SERVER_NAME' => headers.fetch('Host', 'localhost'), 'SERVER_PORT' => headers.fetch('X-Forwarded-Port', 443).to_s, 'rack.version' => Rack::VERSION, 'rack.url_scheme' => headers.fetch('CloudFront-Forwarded-Proto') { headers.fetch('X-Forwarded-Proto', 'https') }, 'rack.input' => StringIO.new(body), 'rack.errors' => $stderr, } headers.each_pair do |key, value| name = key.upcase.gsub '-', '_' header = case name when 'CONTENT_TYPE', 'CONTENT_LENGTH' name else "HTTP_#{name}" end env[header] = value.to_s end begin status, headers, body = $app.call env body_content = "" body.each do |item| body_content += item.to_s end response = { 'statusCode' => status, 'headers' => headers, 'body' => body_content } if event['requestContext'].has_key?('elb') response['isBase64Encoded'] = false end rescue Exception => exception response = { 'statusCode' => 500, 'body' => exception.message } end response endtemplate.yamlAWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Resources: SinatraFunction: Type: 'AWS::Serverless::Function' Properties: FunctionName: SlackBot Handler: lambda.handler Runtime: ruby2.5 CodeUri: './' MemorySize: 512 Timeout: 30 Events: SinatraApi: Type: Api Properties: Path: / Method: ANY RestApiId: !Ref SinatraAPI SinatraAPI: Type: AWS::Serverless::Api Properties: Name: SlackBotAPI StageName: Prod DefinitionBody: swagger: '2.0' basePath: '/Prod' info: title: !Ref AWS::StackName paths: /{proxy+}: x-amazon-apigateway-any-method: responses: {} x-amazon-apigateway-integration: uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SinatraFunction.Arn}/invocations' passthroughBehavior: 'when_no_match' httpMethod: POST type: 'aws_proxy' /: get: responses: {} x-amazon-apigateway-integration: uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SinatraFunction.Arn}/invocations' passthroughBehavior: 'when_no_match' httpMethod: POST type: 'aws_proxy' ConfigLambdaPermission: Type: 'AWS::Lambda::Permission' DependsOn: - SinatraFunction Properties: Action: lambda:InvokeFunction FunctionName: !Ref SinatraFunction Principal: apigateway.amazonaws.com Outputs: SinatraAppUrl: Description: App endpoint URL Value: !Sub "https://${SinatraAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod/"それぞれが何を表しているのかについてここでは割愛。(この辺はawsの公式ドキュメントをほぼそのまま使っているため)
参照: aws-samples/serverless-sinatra-sampleS3バケットの作成
事前にAWS S3バケットを準備しておく必要があるため、適当にS3バケットを作成。
参照: AWS S3のバケットの作り方
デプロイ
次のコマンドを実行。
$ aws cloudformation package \ --template-file template.yaml \ --output-template-file serverless-output.yaml \ --s3-bucket # 先ほど作成したS3バケット名 Uploading to a3a55f6abf5f21a2e1161442e53b27a8 12970487 / 12970487.0 (100.00%) Successfully packaged artifacts and wrote output template to file serverless-output.yaml. Execute the following command to deploy the packaged template aws cloudformation deploy --template-file /Users/ユーザー名/ディレクトリ名/serverless-output.yaml --stack-name <YOUR STACK NAME>すると、ディレクトリ内に「serverless-output.yaml」というファイルが自動生成されているはずなので、こちらを元に次のコマンドを実行。
$ aws cloudformation deploy --template-file serverless-output.yaml \ --stack-name slack-bot \ --capabilities CAPABILITY_IAM Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - slack-bot「Successfully」と表示されればデプロイ成功。
「Lambda」→「関数」と進むと先ほどデプロイした内容が表示されるので、「API Gateway」内に記載されているAPIエンドポイントにアクセス。
./main.rbget '/' do 'This is SlackBot on AWS Lambda' endmain.rb内のget '/' リクエストの期待通り「This is SlackBot on AWS Lambda」が返ってくれば正常に動作していると判断してOK。
Slash CommandsのRequest URLを変更
再びSlackBotの設定ページにアクセスし、「Slash Commands」からRequest URLを先ほど作成されたエンドポイントに変更する。(https://********.execute-api.ap-northeast-1.amazonaws.com/Prod/webhook」)
最後にもう一度、Slackチャンネルで「/nerima」と打ち込み、ちゃんとレスポンスが返ってくればめでたしめでたし。
もし上手く行かなかった場合はCloudWatchにログが出力されているはずなので、適宜デバッグ。あとがき
お疲れ様でした!
今回、簡単なSlackBotをLambdaで動かすというテーマでAWS Lambdaに触れてみました。
大した機能は実装できていないのでLamdaの素晴らしさを全て実感というわけにはいきませんでしたが、上手く使えばかなり便利なサービスだと素人ながら感じています。難しい操作はしていないため、基本的には手順通りに進めていけば動くはずですが、もし詰まるところがあった場合はコメント欄などで指摘していただけると嬉しいです。
- 投稿日:2020-09-21T22:04:08+09:00
Rails 6で認証認可入り掲示板APIを構築する #16 policyの設定
←Rails 6で認証認可入り掲示板APIを構築する #15 pundit導入
post_policyの編集
まずは
spec/spec_helper.rb
を以下のように変更します。
punditの公式にあるように、以下の通り追加すればpundit用のrspecメソッドが使えるようになります。spec/spec_helper.rbRSpec.configure do |config| ... + require "pundit/rspec" end次に
spec/policies/post_policy_spec.rb
にテストを組み込んでいきます。spec/policies/post_policy_spec.rb# frozen_string_literal: true require "rails_helper" RSpec.describe PostPolicy, type: :policy do let(:user) { create(:user) } let(:post) { create(:post) } subject { described_class } permissions :index?, :show? do it "未ログインの時に許可" do expect(subject).to permit(nil, post) end end permissions :create? do it "未ログインの時に不許可" do expect(subject).not_to permit(nil, post) end it "ログインしている時に許可" do expect(subject).to permit(user, post) end end permissions :update?, :destroy? do it "未ログインの時に不許可" do expect(subject).not_to permit(nil, post) end it "ログインしているが別ユーザーの時に不許可" do expect(subject).not_to permit(user, post) end it "ログインしていて同一ユーザーの時に許可" do post.user = user expect(subject).to permit(user, post) end end endなんとなく読み解けると思いますが、念の為解説を。
permissions :index?, :show? do it "未ログインの時に許可" do expect(subject).to permit(nil, post) end endindex?とshow?は条件が同じのためまとめてテストをしています。
permit(nil, post)
は第1引数にログインユーザーmodelを、第2引数に対象modelを指定します。
すると第1引数のユーザーが第2引数のオブジェクトのindex?やshow?の権限があるかテストをします。permissions :update?, :destroy? do ... it "ログインしているが別ユーザーの時に不許可" do expect(subject).not_to permit(user, post) end it "ログインしていて同一ユーザーの時に許可" do post.user = user expect(subject).to permit(user, post) end endnot_toは見たままですが、許可されていないことのテストですね。
そして、postの所有ユーザーを一致したことで最後のテストはパスします。request specの修正
一旦rspecを動かしてみます。
するとspec/requests/v1/posts_request_spec.rb
が結構コケます。原因は上記と同じく、#updateや#destoryが所属ユーザーのログインじゃないと403になるからです。
ですが、posts_request_spec.rb
で使っているauthorized_user_headers
ヘルパは内部でcreate(:user)
をしているため、ログインユーザーとpostユーザーを一致できません。そのため、以下のように修正を加えます。
spec/support/authorization_spec_helper.rbmodule AuthorizationSpecHelper - def authorized_user_headers - user = create(:user) + def authorized_user_headers(user = nil) + user = create(:user) if user.nil? post v1_user_session_url, params: { email: user.email, password: "password" }これで、
authorized_user_headers
に引数無しで渡した場合は内部でuserが作られ、引数でuserを渡した場合はそれを利用します。ただし
authorized_user_headers
が少し複雑になってしまいrubocopのAbcSizeに引っかかるので、以下の対応をします。.rubocop.yml... + +# AbcSize デフォルト15はキツいので20に上げる +Metrics/AbcSize: + Max: 20
さて、ようやくrequest specの修正です。
spec/requests/v1/posts_request_spec.rb... it "正常レスポンスコードが返ってくる" do - put v1_post_url({ id: update_param[:id] }), params: update_param + post = Post.find(update_param[:id]) + put v1_post_url({ id: update_param[:id] }), params: update_param, headers: authorized_user_headers(post.user) expect(response.status).to eq 200 end it "subject, bodyが正しく返ってくる" do - put v1_post_url({ id: update_param[:id] }), params: update_param + post = Post.find(update_param[:id]) + put v1_post_url({ id: update_param[:id] }), params: update_param, headers: authorized_user_headers(post.user) json = JSON.parse(response.body) expect(json["post"]["subject"]).to eq("update_subjectテスト") expect(json["post"]["body"]).to eq("update_bodyテスト") end it "不正パラメータの時にerrorsが返ってくる" do - put v1_post_url({ id: update_param[:id] }), params: { subject: "" } + post = Post.find(update_param[:id]) + put v1_post_url({ id: update_param[:id] }), params: { subject: "" }, headers: authorized_user_headers(post.user) json = JSON.parse(response.body) expect(json.key?("errors")).to be true end @@ -106,13 +109,13 @@ RSpec.describe "V1::Posts", type: :request do create(:post) end it "正常レスポンスコードが返ってくる" do - delete v1_post_url({ id: delete_post.id }) + delete v1_post_url({ id: delete_post.id }), headers: authorized_user_headers(delete_post.user) expect(response.status).to eq 200 end it "1件減って返ってくる" do delete_post expect do - delete v1_post_url({ id: delete_post.id }) + delete v1_post_url({ id: delete_post.id }), headers: authorized_user_headers(delete_post.user) end.to change { Post.count }.by(-1) endそこまで大きな変更は無いですね。
authorized_user_headers
にpostの所有ユーザーを渡すことで、認可を通過します。所有ユーザー一致判定メソッドの作成
ここをもう少し直感的に変えていきます。
自分自身のものか判定する処理はpostに限らず、今後もいろいろなmodelで流用しそうですよね。def update? @record.user == @user end def destroy? @record.user == @user endそのため、application_policy.rbに自分自身のものか判定するプライベートメソッドを作ります。
app/policies/application_policy.rbclass ApplicationPolicy ... + private + + def mine? + @record.user == @user + end # # scope # class Scope ...post_policyに反映してみます。
app/policies/post_policy.rbdef update? - @record.user == @user + mine? end def destroy? - @record.user == @user + mine? endスッキリしましたね。
これで、今後はuserの関連を持つmodelが自身が所有している場合のみ実行するactionは、policyファイルを作ってmine?
メソッドを配置するだけ。超お手軽ですね。続き
- 投稿日:2020-09-21T21:05:27+09:00
"Rails"でのフォーマット環境を整える(VScode)
この記事を書いたきっかけ
Railsで開発を進めるにあたり、VScodeで開発を進めていたのですが、
Rubocopだけではなかなか思っていた通りにフォーマットが動作せず、四苦八苦しました。
Railsでの環境下では最低限.rbファイルと.erbファイルのフォーマットが必要となります。
さらに他の言語(JavaScript,yaml等)へフォーマッター対応も拡張できる方が便利です。他エディターはどうか知りませんが、VScodeでの構築はポイントがあるように感じます。
初心者ゆえに間違えがありましたが、やさしくご指摘頂ければと思います。
この記事の目的
- 安定したRailsのコーディング環境(Ruby,erbのフォーマット環境)を整える
- 解析ツール・フォーマッターであるRubocopと共に使用できる
- 他の言語の拡張も可能なフォーマッターの導入と安定稼働
検証環境
Rails v6.0.3.3
Ruby v2.6.6
Rubocop v0.89.1・拡張機能
Ruby v0.27.0
Prettier v5.6.0
Prettier+ v4.2.2
Beautify v1.5.0・インストール
prettier/plugin-ruby v0.20.01.VScodeの設定(setting json)
フォーマットを整える前にエディターに下記の設定を入れてあります。
エディターの設定画面はCommand + ,
で表示できます。
settings.jsonのファイルを開いて下記の設定を入れ込みます。
settings.jsonは設定画面右上にあるアイコンに合わせると"設定(JSON)"と表示されるアイコンです。
アイコンをクリックして下記設定を入れると
- 末尾の空白を除去し、最終行に新しい行を入れる設定
- 自動フォーマット
が反映されます。
settings.json{ "files.trimFinalNewlines": true, #ファイルの保存時に最終行以降の新しい行をトリミング "files.insertFinalNewline": true, #ファイルの保存時に最新の行を末尾に追加 "files.trimTrailingWhitespace": true, #末尾の空白をトリミング "editor.formatOnSave": true, #ファイル保存時に自動フォーマット }1.Rubyのフォーマット環境
手順としては、まずgemでrubocopがインストールされている前提で、
VScodeの拡張機能で
- Prettier
- Prettier+
をインストールします。*忘れず2つともインストールしてください。その後にrailsの開発プロジェクト内でprettierのプラグインをインストールします。
フォーマッターのPrettier,Prettier+は標準ではRubyに対応しておらず、プラグインでの対応です。
早速ですが、公式サイトに記載ある通りプラグインのインストールと実行を行ってみます。
公式サイト:plugin-ruby私の環境はRails6ですので、yarnでインストールしました。
yarn add --dev prettier @prettier/plugin-ruby`Rails環境下の一斉実行は下記のコマンドで実行できます。
./node_modules/.bin/prettier --write '**/*.rb'これで既存のRubyファイルのフォーマットが完了しました。
保存後、エディター、Dockerなど関連ソフトは全て再起動を行ってください。再起動後に何らかのrbファイルを開いてエディター右下にPrettier,Prettier+のチェックが入っている事を確認ください。
その上で、ファイルに空白文字やいくつか改行などを入れてから保存しフォーマットが実行されるか確認してみてください。
うまく実行できればRubyのフォーマット設定は完了です。2.erbのフォーマット
拡張機能のbeautifyをインストールします。
Prettierがあるのになんで? という意見があると思うのですが、
実際にそれでも構わないのですが、下記のように設定した場合、一部のレイアウトが削除されてerbが分かりづらくなります。
これはerbがhtmlで認識されている為に発生します。Prettier サンプル例
settings.json"files.associations": { "*.html.erb": "html" }Prettier での表示例(ハイライトが消えてわかりづらい)
beautifyを拡張機能でインストールしてsettings.jsonで下記の通りに設定を入れます。
"beautify.language": { "html": ["htm", "html", "erb"] },これでhtmlのフォーマットがerbにも適応されつつ、erbのハイライトが表示されてわかりやすく作業ができます。
3.JavaScriptのフォーマット
JavaScriptのフォーマット設定はPrettierの設定をjsonに反映するだけでOKです。
settings.json"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },追記事項
VScodeでのRailsフォーマット環境の一例を紹介しました。
各種設定詳細は割愛しましたので、細かな設定については公式サイト等を参照頂ければと思います。
上記設定でうまく動かない場合は他の拡張機能が邪魔をしている場合がありますので、
拡張機能をいったん無効にして動作確認してみて下さい。最後に
私の構築した環境がベストな構築方法とは思いませんが、
Rails環境を構築の一例として、構築したフォーマット環境が他の方に少しでも参考になれば幸いです。
- 投稿日:2020-09-21T21:01:29+09:00
Ruby on Rails バリデーションまとめ
はじめに
主にRails Tutorialの備忘録として、例などをまとめてみます。
間違い等ありましたらご指摘いただけますと幸いです。バリデーションとは
オブジェクトをデータベースに保存する前に、オブジェクトの状態を検証することです。
入力された値が無効ではないことを検証します。
例えば、空のデータが保存されないようにしたり、文字数に制限を儲けたりすることができます。基本的な書き方
基本的な記入方法は以下です。
modelvalidates :カラム名, ヘルパー #複数のカラムに適用したい場合 validates :カラム名, :カラム名, :カラム名, ヘルパー主なバリデーションのヘルパー
presence
指定された属性が空でないことを確認します。
modelvalidates カラム名, presence: truemodelvalidates :name, presence: true #複数のカラムに適用したい場合 validates :name, :login, :email, presence: truelength
属性の値の長さを検証します。多くのオプションがあり、さまざまな長さ制限を指定できます。
modelvalidates カラム名, length: { 制限 }modelvalidates :passward, length: { minimum: 5 } #5文字以上 validates :name, length: { maximum: 50 } #20文字以下 validates :passward, length: { in: 3..10 } #3文字以上10文字以下 validates :registration_number, length: { is: 6 } #6文字のみ許可uniqueness
属性の値が一意(unique)であり重複していないことを検証します。
modelvalidates :カラム名, uniqueness: truemodelvalidates :name, uniqueness: trueacceptance
フォームが送信された時に、チェックボックスがオンになっているかどうかを検証します。
「サービスの利用規約に同意する」にチェックする必要がある場合などに利用されます。modelvalidates :カラム名, acceptance: truemodelvalidates :terms_of_service, acceptance: trueconfirmation
複数のフォームで入力された値が完全に一致するかどうかを検証します。
メールアドレスと確認用メールアドレスの値の一致などに利用されます。modelvalidates :カラム名, confirmation: truemodelvalidates :email, confirmation: truenumericality
属性に数値のみが使われていることを検証します。
modelvalidates カラム名, numericality: truemodelvalidates :points, numericality: truemodel#整数のみ許可 validates :age, numericality: { only_integer: true }主なオプション
オプション 概要 :only_integer 整数でなけえればならない :equal_to 指定された値と等しくなければならない format
withオプションで与えられた正規表現と属性の値がマッチするかどうかのテストによる検証を行います。
modelvalidates :カラム名, format: { with: 制限 }model#英数字のみ許可 validates :password, format: { with: /\A[a-zA-Z]+\z/ } #有効なメールアドレスのみ許可 VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i validates :email, format: { with: VALID_EMAIL_REGEX }上記のような、有効なメールアドレスのみ許可するための正規表現は以下のような表に基づいています。
正規表現 概要 z\d-.]+.[a-z]+\z/i 完全な正規表現 / 正規表現の開始を示す \A 文字列の先頭 [\w+-.]+ 英数字、アンダースコア(_)、プラス(+)、ハイフン(-)、ドット(.)のいずれかを少なくとも1文字以上繰り返す @ アットマーク [a-z\d-.]+ 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す . ドット [a-z]+ 英小文字を少なくとも1文字以上繰り返す \z 文字列の末尾 / 正規表現の終わりを示す i 大文字小文字を無視するオプション 共通のオプション
1 2 :message バリデーション失敗時に表示されるエラーメッセージを指定できます。指定しない場合、デフォルトのメッセージが表示されます。 :allow_nill 対象の値がnilの場合にバリデーションをスキップします。 :allow_blank 属性の値がblank?に該当する場合(nilや空文字など)にバリデーションがパスします。 model# メッセージを直書きする場合 validates カラム名, presence: { message: “カスタムエラーメッセージ” }modelvalidates :name, presence: { message: “必須項目です” }参考
Active Record バリデーション - Railsガイド v6.0
https://railsguides.jp/active_record_validations.html
- 投稿日:2020-09-21T20:44:22+09:00
C++を使ってRubyのメソッドを書く(その2) ベンチマーク
目次
C++を使ってRubyのメソッドを書く(その1)
C++を使ってRubyのメソッドを書く(その2) ベンチマーク <- ここベンチマーク
配列の合計を計算する単純なプログラムです。上記のその1で書いた方法(1)と
Cを使ってRubyのメソッドを書く(その2)Numo::NArray (2)の速度を比較してみました。
他にも、 Numo::SFloat型のデータを渡した場合(3) (Numo::DFloatにキャスト後計算)、
NArrayをArrayに変換してから(1)と同じ方法(4)、
ArrayをNumo::DFloatに変換してから(2)と同じ方法(5)、
Rubyの組込み関数Array#sumを使う方法(6)、
Rubyの組込み関数Array#injectを使う方法(7)、
Numo::DFloatの関数sumを使う方法(8)
の速度を比較しています。結果は最後に書いています。
その1の方法(1)でも十分な速度がありますが、(2)が最高速でした。(6)のArray#sumがかなり速く、バイナリで書かれたRubyの関数は速度で期待できそうです。Numo::DFloat#sumが意外と遅いのとArrayからNumo::DFloatへの変換も遅めのような気がします。
使用したプログラム
test.cpp#include "test.hpp" double sum(const std::vector<double>& ary){ double sum=0.0; for (int i=0; i<ary.size(); i++){ sum+=ary[i]; } return(sum); } double sum_nd(int n, double *ary){ double sum=0.0; for (int i=0; i<n; i++){ sum+=ary[i]; } return(sum); }test.hpp#include <vector> double sum(const std::vector<double>& ary); double sum_nd(int n, double *ary);test.i%module testF %{ #include "numo/narray.h" #include "test.hpp" %} %include <std_vector.i> %template(DoubleVector) std::vector<double>; extern double sum(std::vector<double> ary); %typemap(in) (int LENGTH, double *NARRAY_in){ narray_t *nary; if (rb_obj_class($input)!=numo_cDFloat){ $input = rb_funcall(numo_cDFloat, rb_intern("cast"), 1, $input); } GetNArray($input, nary); if (NA_TYPE(nary)==NARRAY_VIEW_T){ $input = rb_funcall($input, rb_intern("dup"), 0); GetNArray($input, nary); } $2 = ($2_ltype)na_get_pointer_for_read($input); $1 = NA_SIZE(nary); } extern double sum_nd(int LENGTH, double *NARRAY_in);extconf.rbrequire 'mkmf' dir_config("numo/narray") have_header("numo/narray.h") create_makefile("testF")swig -c++ -ruby test.i ruby extconf.rb -- --with-numo/narray-include=/opt/lib/ruby/gems/2.6.0/gems/numo-narray-0.9.1.8/lib/numo/ makeベンチマーク用プログラム
benchmark.rbrequire "benchmark" require "numo/narray" require "./testF" data=[*1..100].map(&:to_f) data_na=Numo::DFloat.cast(data) data_naf=Numo::SFloat.cast(data) puts Benchmark::CAPTION puts Benchmark.measure{ 1000000.times{ TestF::sum(data) # (1) } } =begin 以下 TestF::sum_nd(data_na) # (2) TestF::sum_nd(data_naf) # (3) TestF::sum(data_na.to_a) # (4) TestF::sum_nd(Numo::DFloat.cast(data)) # (5) data.sum # (6) data.inject(:+) # (7) data_na.sum # (8) =end結果
データの流れ 実行時間 (μs) (1) Array => vector 2.44 (2) Numo::DFloat => double [] 0.14 (3) Numo::SFloat => cast -> double [] 1.59 (4) Numo::DFloat -> Array => vector 4.77 (5) Array -> Numo::DFloat => double [] 10.11 (6) Array#sum 0.60 (7) Array#inject(:+) 3.42 (8) Numo::DFloat#sum 1.30 (実行時間はループ一回分に換算しています)
- 投稿日:2020-09-21T20:11:07+09:00
【Rails】初心者でも頻繁に使うActive Recordメソッド
Active Recordとは
SQLを書かずに、データベースのデータを扱えるようにすることができるRuby on Railsの機能。
これをORマッパー
と呼んだりする。
Active Recordを使えば、SQL文を書かずに簡単なメソッドでデータを扱うことが可能になる。ちなみにRailsに関わらず、PHPのフレームワークでもこのORマッパーを採用している。
User.all
users = User.allすべてのユーザーの情報を取得する。
User.find
user = User.find(1)主キーが一致するユーザーのレコードを取得する。
User.find_by
user = User.find_by(name: 'gorira')指示した条件でカラムのデータが一致する最初の1つのレコードを取得する。
User.first
user = User.firstユーザーの最初の1件のレコードを取得する。
User.where
user = User.where(name: 'takeshi')条件が一致する全てのレコードを取得する。
User.pluck
users_name = User.pluck(:name)特定のカラムを配列で取得する。
User.order
user = User.order('id DESC')指定したカラムを基準に並び替える。
User.delete_all
user = User.delete_allユーザーテーブルのデータをすべて削除する。
User.eager_load
user = User.eager_load(:items)外部結合し、全てのデータを取得する。
参考記事
- 投稿日:2020-09-21T19:36:49+09:00
【Ruby】これで解決。インスタンス変数っていつ使えばいいの?
読んでほしい人
インスタンス変数の意味はなんとなくわかったけど、結局どうローカル変数・インスタンス変数・クラス変数を使い分けるの?
という過去の自分のような人へ。
初学者でもこれさえ読めばわかるように書けていると思います。結論:スコープです
どこの範囲で使いたい変数なのかを考えるだけ。
つまりスコープ(変数やメソッドを呼び出せる範囲)を考えればよいのです。
以下で解説します。Rubyで使える変数の種類
1.ローカル変数
2.インスタンス変数
3.クラス変数
4.グローバル変数それぞれスコープが違います。
もしスコープ外で呼び出そうとしたら下記のように「〇〇〇っていう変数orメソッドなんて定義されてないよ!」とエラーが出ます。
undefined local variable or method `〇〇〇' for main:Object (NameError)
ローカル変数
変数名の前に@も何もつけません。定義したメソッドやクラスの中だけで呼び出せます。
例1 変数morningはmorning_messageメソッドの中だけで使える
local_variable.rbdef morning_message morning = "朝" puts "#{morning}です" end morning_message # => 朝です puts morning # => undefined local variable or method `morning' for main:Object (NameError)例2 Morningクラスの中かつself.messageの外側だけで使える
local_variable.rbclass Morning morning = "朝" def self.message puts "#{morning}です" end self.message # => undefined local variable or method `morning' for Morning:Class (NameError) endインスタンス変数
頭に@をつけます。同じクラスのメソッドの中ならどこでも呼び出せます。
ちなみにinitializeメソッド以外で定義することもできます。例1 initializeメソッドで定義した
@color
をput_colorメソッドでも使えるinstance_variable.rbclass Color def initialize(color) @color = color end def put_color puts @color end end color1 = Color.new("青") color1.put_color # => 青例2 initializeメソッド以外でもインスタンス変数を定義
instance_variable.rbclass Color def initialize(color) @color = color end def rank if @color == "青" @rank = "A" elsif @color == "赤" @rank = "B" end end def explain puts "#{@color}のランクは#{@rank}です" end end color1 = Color.new("青") color2 = Color.new("赤") color1.rank color1.explain # => 青のランクはAです color2.rank color2.explain # => 赤のランクはBですクラス変数
頭に@@をつけます。クラス内のどこでも呼び出せます。
インスタンス変数はnewメソッドでインスタンスを作成する度に値が更新されるのに対して
クラス変数はクラス内で共通の値になります。例
@@monitor
はcolor1,2で共通class_variable.rbclass Color @@monitor = "モニター1" def initialize(color) @color = color end def rank if @color == "青" @rank = "A" elsif @color == "赤" @rank = "B" end end def explain puts "#{@@monitor}の#{@color}のランクは#{@rank}です" end end color1 = Color.new("青") color2 = Color.new("赤") color1.rank color1.explain # => モニター1の青のランクはAです color2.rank color2.explain # => モニター1の赤のランクはBですグローバル変数
頭に$をつける。プロジェクト内ならどこでも呼び出せます。
スコープが広すぎるがゆえに意図せず変数名が被り、内容が書き換えられてしまうことがあるので、どうしてもというときだけ使うようにしましょう例 $brightnessがどこでも呼び出せる&書き換えられる
global_variable.rb$brightness = 2000 class Color def initialize(color) @color = color end def explain puts "その#{@color}の輝度は#{$brightness}ntです" end end color1 = Color.new("青") color2 = Color.new("赤") color1.explain # => その青の輝度は2000ntです $brightness = 100000 color2.explain # => その赤の輝度は100000ntですさいごに
この記事についての感想・意見・指摘などあれば伝えて頂けると嬉しいです。
参考元
https://techacademy.jp/magazine/9704
https://qiita.com/tomokichi_ruby/items/a2548176d85457f622a4
- 投稿日:2020-09-21T19:15:39+09:00
【Reactで】小説投稿サイトなどに良くある「全○話」みたいなのを表示させる方法
はじめに
今回は小説投稿サイトなどによくある「全○話」を自作アプリにて表示させるのに結構苦労した(3日かかった)ので、戒めとして残しておきたいと思います。
実現したかったこと
用意したのは
「シリーズ」
というフォルダ的な役割を持つモデルと、「アイテム」
というシリーズに複数個格納されるモデルの2つ(1対多の関係)
ルートページ
にて「シリーズ」全件を表示させ、その「シリーズ」が所有する「アイテム」を全て取得し、その総数をカウント
させ「全〜件」という形で表示
させたい。苦労した理由
- 表示させたいのが
ルートページ
だったからです。普通なら各シリーズが所有するアイテム
を取得しようとする場合、例えばURLが"/series/104"
なら、シリーズのパラメータ(この場合なら104)を取得して、そのパラメータを頼りにアイテムを取得します。なので、パラメータが存在しないルートページでどうやって各シリーズのパラメータを取得すりゃええんじゃいとと半ばキレかけながら考えていたわけです(今思えば単純な話でした)
環境・前提等
環境
フロントエンド
- React(v16.8以上)
- React Hooks(カスタムフックを使う)
- axios
バックエンド
- Rails(5.2系)
前提
- CORSの設定、モデル作成などの工程は省略します。
- PUMAでRails側のローカルホストをデフォルトで3001に指定しています。
Rails側
- モデル
コントローラ
Api::V1::SeriesController
- このコントローラにてシリーズ全件を返すアクションと、アイテムのカウントを返すアクションを作成する。
ルーティング
- ルート:
"/"
→"api/v1/series#index"
- アイテム取得:
"api/v1/item_count/:series_id"
→"api/v1/series#item_count"
React側
- 用意するコンポーネント
Homeコンポーネント
: シリーズを全件取得し、Seriesというコンポーネントに各データを順繰り渡す役割りを持たせる。Seriesコンポーネント
: このコンポーネントにて各シリーズを表示させる。ItemCountコンポーネント
: 各シリーズが持つアイテムの総数だけを表示させる。useFetchカスタムフック
: Railsからデータを取得する。Rails側のコード
ルーティング
routes.rbRails.application.routes.draw do # ルート root to: 'api/v1/series#index' # アイテムのカウント get 'api/v1/item_count/:id', to: 'api/v1/series#item_count' endコントローラ
app/controller/api/v1/series_controller.rbclass Api::V1::SeriesController < ApplicationController # item_countアクションに、パラメータから取得したシリーズをコールバック before_action :set_series, only: [:item_count] def index @series =Series.all render json: { status: 200, series: @series, keyword: "index_of_series" # React側で使う } end def item_count @items = @series.items.all # シリーズに関連付けられているアイテムの取得 @items_count = @items.count # アイテムの総数をカウント render json: { status: 200, item_count: @item_count, # カウントをJSONとしてReactへ送信 keyword: "item_count" # React側で使う } end private # パラメータを頼りにシリーズを取得 def set_series @series = Series.find(params[:id]) end endReact側のコード
// 階層 //src // ├ Home.js // ├ Series.js // ├ ItemCount.js // └ useFetch.jsuseFetchカスタムフック
src/useFetch.jsimport { useState, useEffect } from "react" import axios from 'axios' // カスタムフックでは文頭はuseが必須 // useFetchの引数に、methodとurlを渡す // これは、HomeとItemCountコンポーネントにて、Railsとの通信に使う // HTTPリクエストと、ルーティングを指定するため export default function useFetch({method, url}) { // 初期値の定義。 const [items, setItems] = useState("") useEffect(() => { const getItems = () => { // ここのmethodとurlにて、Home・ItemCountコンポーネントから // 送られてくるメソッドとルーティングを代入することになる。 axios[method](url) .then(response => { let res = response.data let ok = res.status === 200 // シリーズ全件取得 // Rails側で指定したkeywordはここで使う。 // そうしてカウントとの区別を付けている。 if (ok && key === 'index_of_series') { setItems({ ...res.series }) // シリーズごとのアイテムの総数を取得 } else if (ok && key === 'item_count') { setItems(res.item_count) } }) .catch(error => console.log(error)) } getItems() }, [method, url, items]) return { items // items変数を他のコンポーネントで使えるようにする。 } }Homeコンポーネント
src/Home.jsimport React from 'react' import Series from './Series' import useFetch from './useFetch' function Home() { // ここでは、useFetchからRailsで取得したシリーズのデータを受け取っている。 // methodはget、urlはRailsのルートのURLを指定。これにより、 // useFetchからRailsのルートのルーティングへリクエストが送信され、 // その後Railsから受け取ったデータをitemsへ格納します。 const { items } = useFetch({ method: "get", url: 'http://localhost:3001' }) return ( <div> {/* Object.keys()メソッドを使い、JSONで送られてくるitemsを */} {/* ループ処理で1個ずつSeriesコンポーネントに渡している。 */} {/* JSONは、{ {...}, {...}, {...} }のようなものであると想定 */} {Object.keys(items).map(key => ( <Series key={key} items={items[key]} /> ))} </div> ) } export default HomeSeriesコンポーネント
src/Series.jsimport React from 'react' import ItemCount from './ItemCount' function Series(props) { // Homeから送られてくるpropsを頼りに、各シリーズのidをここで取得しています。 // このidをパラメータとして使うことで、各シリーズの所有するアイテムにアクセスすることができます。 const seriesId = props.items.id const seriesTitle = props.items.title return ( <div> <div>{seriesTitle}</div> {/* ItemCountコンポーネントに、シリーズのidを渡す。 */} <ItemCount {...props} seriesId={seriesId} /> </div> ) } export default SeriesItemCountコンポーネント
src/ItemCount.jsimport React from 'react' import useFetch from './useFetch' function SeriesCount(props) { // useFetchを使いRailsと通信。 // methodはget、urlはRailsの`api/v1/item_count/${props.seriesId}`を指定。 // id部分にSeriesコンポーネントから渡ってくる各シリーズのidを嵌め込むことで、 // Railsの"api/v1/item_count/:id"というルーティングへリクエストが送信され、 // その後Railsから各シリーズの持つアイテムのカウント数を受け取り、最後にitemsへ格納されます。 const { items } = useFetch({ method: 'get', url: `http://localhost:3001/api/v1/item_count/${props.seriesId} ` }) return ( <div> {/* Railsから送られてくるアイテムの総数をここにレンダリングします。 */} (このシリーズは全部で {items} 個のアイテムを所有しています) </div> ) } export default SeriesCount
- 投稿日:2020-09-21T18:24:32+09:00
rails チュートリ
Rubyのバージョン番号
ところで、Herokuのデプロイするとき、もしかしたら次のような警告メッセージを目にしたことがあるかもしれません。WARNING:
You have not declared a Ruby version in your Gemfile. To set your Ruby version add this line to your Gemfile: ruby '2.1.5'(これは「Rubyのバージョンを明示的に指定してください」というメッセージですが) 経験的には、本書のようなチュートリアルの段階では明示的に指定しない方がスムーズに進むことが多いので、この警告は現時点では無視してしまった方がよいでしょう。というのも、サンプルアプリケーションでRubyのバージョンを常に最新に保っておくと、多大な不都合に繋がりかねないからです15 。また、本書のサンプルアプリケーションにおいては、ローカルで使っているバージョンと本番環境のバージョンが異なっていても、違いが生じることはほぼ無いでしょう。とは言うものの、次の点は頭の片隅に置いておいてください。それは、仕事でHerokuを使ったアプリケーションを動かす場合はGemfileでRubyのバージョンを明示しておいた方が賢明である、という点です。これによって開発環境と本番環境の互換性を最大限に高めることができるので、(バージョンの差異による誤作動やエラーなどが無くなり) オススメです。
- 投稿日:2020-09-21T18:20:31+09:00
特異メソッドについて
はじめに
Rubyの特異メソッドと特異クラスについて備忘録としてまとめました。
特異メソッド
Rubyではオブジェクトに対して直接固有のメソッドを定義することができて、そのメソッドを特異メソッドといいます。
class Hoge end obj = Hoge.new def obj.method1 p '特異メソッド' end obj.method1 # => "特異メソッド"このように
method1
が呼ばれたことを確認することができたと思います。
では、別のオブジェクトではどのような結果になるのでしょうか?obj2 = Hoge.new obj2.method1 # => undefined method `method1'このようにエラーになってしました。 同じクラスでも
obj
固有のメソッドであることが確認できたと思います。特異メソッドの正体
同じクラスでも特異メソッドを定義してないオブジェクトであればメソッドを呼ぶことはできないことが確認できたと思います。念のために、
Hoge
クラスに特異クラスが本当にないことをsingleton_methods
を使って確認してみましょう。class Hoge end obj = Hoge.new def obj.method1 p '特異メソッド' end p Hoge.singleton_methods p obj.singleton_methods # => [] # => [:method1]しかし、メソッドはクラスに属するものだとしたら特異メソッドはどのクラスに属しているか気になり始めた頃かもしれません。調べた結果、特異クラスというものが存在していてそのクラスに特異メソッドは属しているみたいです。
特異クラス
正直、特異クラスは特異メソッドが定義されているものという理解しかしていません。
ancestors
を使って中身を確認してみましょう。p obj.singleton_class.ancestors # => [#<Class:#<Hoge:0x00007fca5c866ab8>>, Hoge, Object, Kernel, BasicObject]このように特異クラスのスーパークラスは自分のクラスのようです。
メソッド探索は 特異クラス → 自身のクラス → スーパークラス ... の順になっているみたいです。
- 投稿日:2020-09-21T15:18:11+09:00
[Rails]flashメッセージの使い方
はじめに
flashとは、ページ遷移した時に簡単なメッセージを一時的に表示させる機能です。
例えば、ユーザーがログインに成功した時に『ログインできました。』と表示させることで、ユーザーが進行具合を確認できるというものです。目次
1 基本的な書き方
2 flashとflash.nowの違い基本的な書き方
コントローラーの編集
flashはハッシュのような形で扱います。
flash[:キー名] = “表示さたいメッセージ”キー名はあらかじめ用意せれているnoticeかalertオプションを用います(自分で好きな名前をつけることも可能です)。
例
controlle.rbif @outgo.update(outgo_params) flash[:alert] = ‘メッセージが’登録されました。’ redirect_to root_path else ~ 略 ~ビューの編集
フラッシュメッセージを表示させたい箇所に下記を記述します。
<%= flash[:キー名] %>例
html.erb<%= flash[:alert] %>flashとflash.nowの違い
両者の使い分け
- flashは次のアクションが動いた後のビューファイルにflashメッセージを表示する時(redirect_toを用いた時)
- flash.nowは現在のアクションで表示するビューファイルのみ有効なflashメッセージを表示させたい時(renderを用いた時)
renderとredirect_toの挙動
- render : controller → view
コントローラーの直後にビューを表示しています。- redirect_to : controller → URL → route → controller → view
コントローラーの直後に一度ルーティングを経由してビューを表示しています。
なので
flashは最初のアクションが実行された後の1回のみflashメッセージが表示されるのでredirect_toメソッドと一緒に用います。
flash.nowは次のアクションが動く間表示されるのでrenderメソッドと一緒に用います。参考リンク
https://pikawaka.com/rails/flash
https://qiita.com/dice9494/items/2a0e92aba58a516e42e9
- 投稿日:2020-09-21T14:57:23+09:00
addEventListenerにより重複している処理を防ぐ方法
動作環境
Ruby 2.6.5
Rails 6.0.3.2以前、addEventListenerによって同じ処理が何度も発生していたことにより、エラーが起きてしまい、思うような処理ができないことがあったので、投稿してみました。
addEventListenerにより重複している処理が発生してしまう例
index.html.erb<% @hugas.each do |huga| %> <div class="huga" > <%= huga.content %> </div> <% end %>上記のコードは、hugaのcontentカラムを繰り返し表示させており、その表示されたcontentのclassはhugaであるということを表しています。
huga.jsfunction hoge() { const hugas = document.querySelectorAll(".huga"); hugas.forEach(function (huga) { huga.addEventListener("click", () => { //クリックすることで発生する処理 }); }); }; setInterval(hoge, 1000);function hoge()を1行目として、2行目からこのコードの解説をしていきます。
2行目でclassがhugaである要素をすべてhugasに代入しています。
3行目でhugasを1つずつに分けて、それらの名前をhugaとしています。
4行目でhugaをクリックすると5行目の処理を発生するようにしています。
最終行により、以上の動作を毎秒発生させています。つまり、上記の2つのコードはcontentをクリックすると、huga.jsの5行目に書かれた処理が発生するということを表しています。
しかし、このままではエラーが起きてしまいます。なぜなら、huga.jsの最終行により毎秒2行目と3行目の動作が発生しているからです。それによって何が起こるのかというと、例えばそのページに遷移してから10秒後にcontentをクリックした場合、5行目の処理が10回同時に発生してしまうというようなことが起きてしまいます。
この問題を解決するために、この記事のタイトルである「addEventListenerにより重複している処理を防ぐ方法」が必要となります。
※そもそもsetIntervalではなく、window.addEventListener('load',hoge);にすれば良いのではという意見が出ると思いますが、その通りです。しかし、index.html.erbのhuga.contentにおいて非同期通信が使われている場合はsetIntervalを使う必要があります。非同期通信だとページのロードが行われないからです。
addEventListenerにより重複している処理を防ぐ方法
huga.jsfunction hoge() { const hugas = document.querySelectorAll(".huga"); hugas.forEach(function (huga) { if (huga.getAttribute("baz") != null) { return null; } huga.setAttribute("baz", "true"); huga.addEventListener("click", () => { //クリックすることで発生する処理 }); }); }; setInterval(hoge, 1000);重複している処理は、上記のコードの4行目から7行目を追記することで防ぐことができる。なぜなら、この追記によって何秒経過してからcontentをクリックしても、1つのhugaには1回の処理しか行わないという意味になるからです。
追記した部分を詳しく解説していきます。
まず、1秒経過すると1回目の処理が行われ、if文によって条件分岐が起きます。hugaはbazという属性(Attribute)は持っていないのでnullとなり、条件式はfalseとなりますが、falseの場合の処理は記載されていないため、そのまま次の処理に移ります。7行目によって、hugaにbazという属性が与えられ、それはtrueとなります。つまり、1回目の処理は記載する前と変わっていません。2秒経過した場合を見ていきます。2秒経過すると2回目の処理が行われ、再度if文によって条件分岐が起きます。1回目と違い、hugaにはbazという属性を持っているため、nullではありません。そのため、条件式はtrueとなり、trueの場合の処理が行われ、return nullが実行されます。return nullとは、処理を抜け出すという意味なので、この記載以降の処理は行われなくなります。つまり、2秒経過してから、contentをクリックしても、処理が1回しか行われないようになります。
当然ですが3秒後以降も同じであるため、重複している処理を防ぐことができます。
- 投稿日:2020-09-21T14:12:53+09:00
Rubyの備忘録(キー・バリューの取得)
Rubyの備忘録(キー・バリューの取得)
ハッシュに含まれるキーやバリューの取得には
予め用意されているメソッドがある。<keysメソッドとvaluesメソッド>
オブジェクト.keys
オブジェクト.valuesqiita.rbputs hash.keys puts hash.valuesこれだと、ハッシュに含まれるすべてのキーやバリューが出力される。
{A: "a"}というハッシュがあるとして、
Aというキーを取り出したいときは、Aと対になる値aを()の中に入れる。
逆にaという値を取り出したいときは、aと対になるキーAを()の中に入れる。qiita.rbhash = { ringo: "apple", mikan: "orange", ichigo: "Strawberry" } puts hash.key("apple") #「ringo: "apple"」の値である"apple"から、キーであるringo:を取得 puts hash.values_at(:ringo) # 「ringo: "apple"」のキーである":ringo"から、値である"apple"を取得配列のように番号で取り出すことはできないんだろうか。
つづく。参考サイト
https://www.javadrive.jp/ruby/hash/index8.html
https://qiita.com/kidach1/items/651b5b5580be40ad047e
- 投稿日:2020-09-21T13:55:48+09:00
【Ruby on Rails】エラーメッセージの個別表示
目標
開発環境
ruby 2.5.7
Rails 5.2.4.3
OS: macOS Catalina前提
※ ▶◯◯ を選択すると、説明等が出てきますので、
よくわからない場合の参考にしていただければと思います。※記述が少なければ、有効な手段かと思いますが、
多くなると記述がかなり増えてしまうので、
参考までに見ていただければと思います。viewの編集
初期状態であれば、下記のように記載されています。
app/views/users/registrations/new.html.erb<h2>Sign up</h2> <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= render "users/shared/error_messages", resource: resource %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name, autofocus: true, autocomplete: "name" %> </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 "users/shared/links" %>上記に編集を加えます。
app/views/users/registrations/new.html.erb<h2>Sign up</h2> <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <% if @user.errors.any? %> <% end %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name, autofocus: true, autocomplete: "name" %> <% if @user.errors.include?(:name) %> <p style="color: red;"><%= @user.errors.full_messages_for(:name).first %> <% end %> </div> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true, autocomplete: "email" %> <% if @user.errors.include?(:email) %> <p style="color: red;"><%= @user.errors.full_messages_for(:email).first %> <% end %> </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" %> <% if @user.errors.include?(:password) %> <p style="color: red;"><%= @user.errors.full_messages_for(:password).first %> <% end %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation, autocomplete: "new-password" %> <% if @user.errors.include?(:password_confirmation) %> <p style="color: red;"><%= @user.errors.full_messages_for(:password_confirmation).first %> <% end %> </div> <div class="actions"> <%= f.submit "Sign up" %> </div> <% end %> <%= render "users/shared/links" %>補足
①下記記述を削除し、if文を追加。@userのエラーを確認します。 <%= form_for〜%> <%= render "users/shared/error_messages", resource: resource %> ↓ <%= form_for〜%> <% if @user.errors.any? %> <% end %> ②各field下に下記のカラム名を編集後追加。 <% if @user.errors.include?(:name) %> <p style="color: red;"><%= @user.errors.full_messages_for(:name).first %> <% end %>バリデーションを追加し、エラーをより多く確認する場合、
下記のように追加すればOK.
バリデーションの種類は多くあるので調べてみてください。
ちなみに下記のバリデーションは空白ではないことを確認しています。app/models/user.rbvalidates :name, presence: true
- 投稿日:2020-09-21T13:36:37+09:00
Ruby 配列やハッシュオブジェクトについてのざっくりメモ
Rubyの配列やハッシュオブジェクトについてのメモです。
配列の基礎的なことは、知っているのでruby特有の書き方をメモしていきたいと思います。配列に要素を追加するとき
num = ["one"] num << "two" p num # ["one", "two"]ハッシュオブジェクトを使っての、キーと値の定義
numKey = { one: 1, two: 2 } p numKey # {:one=>1, :two=>2} p numKey[:two] # 2%記法で配列の定義
# numbers = ["one", "two", "three"] 同じ意味 numbers = %W( one two three ) p numbers # ["one", "two", "three"]配列の中の要素を一つずつ取り出す
%w[one two three].each do |num| p num end # "one" # "two" # "three"元の配列に値を追加して、新しい配列を返す。
nums = %w[one two three four].map{ |num| "数字: #{num}" } p nums # ["数字: one", "数字: two", "数字: three", "数字: four"]
- 投稿日:2020-09-21T11:40:13+09:00
Rubyのエラーメッセージについて
1.はじめに
プログラミング初学者はエラーメッセージの読み解きにどうしても時間がかかってしまいます。とりあえず一番手っ取り早いエラーメッセージを全文コピーしてググる、というやり方をしがちです。しかし、ググればそれらしい情報にがいくつか引っかかりますが、絶対にそれが正しい情報だとは言い切れません。「プロを目指す人のためのRuby入門」で指摘されていますが、ググった結果を実行して、一見うまく解決できたように見えるものでも、実はセキュリティ的に重大な欠陥を生み出していたなんてことがあるそうです。なのでまずはエラーメッセージを読めるようになり、自身で問題点を抽出できるようになることが必要だと考えます。そうすればある程度すばやくバグやエラーを取り除ける上に、Rubyの構造を理解できるようになると考え、この記事を作成しました。
参考にしました→ プロを目指す人のためのRuby入門
2.バックトレースの読み方
プログラム実行中にエラーが発生すると、メソッドの呼び出し状況を表したデータを出力します。これをバックトレースと呼びます。ここではあえて間違った構文を入力しバックトレースを呼び出します。
irb(main):001:0> puts hoge Traceback (most recent call last): 5: from /(Rubyがインストールされているパス)/irb:23:in `<main>' 4: from /(Rubyがインストールされているパス)/irb:23:in `load' 3: from /(Rubyがインストールされているパス)/irb:11:in `<top (required)>' 2: from (irb):1 1: from (irb):2:in `rescue in irb_binding' NameError (undefined local variable or method `hoge' for main:Object)上記はirb(ターミナル上で実行できるRuby)で
puts hoge
という間違った構文を入力したために出力されたバックトレースです。hoge
をシングルクォート(')やダブルクォート(")で囲む等で文字列として認識できなかった為に出た至極単純なエラーです。一つ一つ丁寧に見ていきます。まず
Traceback (most recent call last):ですが、これはエラーメッセージではなく、訳するとトレースバック(最後の最新の呼び出し)、つまりRubyを実行してエラーまでの最新の実行過程を次に表示するよー、という文章です。そして次に
5: from /(Rubyがインストールされているパス)/irb:23:in `<main>' 4: from /(Rubyがインストールされているパス)/irb:23:in `load' 3: from /(Rubyがインストールされているパス)/irb:11:in `<top (required)>' 2: from (irb):1 1: from (irb):2:in `rescue in irb_binding'ですが、エラーまでの実行過程を5:〜1:の流れで表示しています。下に行くほどエラーに近くなっています(言い換えると最新に近くなる)。ここで5:〜3:までは、「irbのプログラムの実行過程」、つづく2:では「irbの1行目を実行」という意味になります。1:は調べましたが
rescue in irb_binding
の意味がわかりませんでした。「irbの2行目で補足がされているか」ということですかね?
つづいて、NameError (undefined local variable or method `hoge' for main:Object)こちらがエラーメッセージになります。まず
NameError
ですが、エラーメッセージの種類です。エラーメッセージの種類については後ほど説明します。続くundefined local variable or method `hoge' for main:Object
ですが、hoge
を文字列と認識できていないため、ローカル変数か定数から探しており、それが定義されていないという意味になります。
今回は単純なミスによるバックトレースなので5行ですんでいますが、複雑なものになると何十行もの実行課程を表示されることがあります。そういった場合でも、冷静に内容をよく読むことが必要です。3.主なエラーメッセージの種類
エラー名 概要 NameError 未定義のローカル変数や定数を使用したときに発生する NoMethodError 存在しないメソッドを呼び出そうとしたときに発生する SyntaxError ソースコードに文法エラーがあったときに発生する TypeError メソッドの引数に期待される型ではないオブジェクトや、期待される振る舞いを持たないオブジェクトが渡された時に発生する SystemStackError システムスタックがあふれたときに発生します。典型的には、メソッド呼び出しを無限再帰させてしまった場合に発生する LoadError require や load が失敗したときに発生する その他のエラーについては、公式リファレンス参照します。
4.エラーメッセージで出会ったときの対処方法
(1)行った手順を確認し、バックトレースを読み込む。
(2)デバッガを使用する。
(3)irbで簡単なコードを動かしてみる。
(4)ログを調べる。
(5)公式ドキュメントを読む。
(6)issueを検索する。
(7)外部ライブラリのコードを読む。
(8)誰かに聞く。
5.まとめ
初学者の私は大量のエラーメッセージを目にするとかなりやる気が削がれてしまいます。エラーメッセージに対しては冷静かつ堅実な対応が必要ですね。エラーに遭遇したときのために、事前に対処法は考えといたほうが良いかもしれないです。
- 投稿日:2020-09-21T11:39:11+09:00
入力した引数に対して、複数の戻り値を返したい
【概要】
1.結論
2.どのように記述するか
3.ここから学んだこと
1.結論
変数を複数用意し、returnで複数用意すること!
2.どのように記述するか
def calculation(num) no1 = (num + 100) * 100 no2 = (num + 10) * 200 return no1,no2 end num = gets_to.i a,b = calculation(num)上記のようにすると変数を複数用意(➡︎a,b)して、returnで複数用意(➡︎no1,no2)しました。そうすると、入力した引数(➡︎num)に対して、複数の戻り値(➡︎no1,no2)を返すことがきます。注意点としては変数は複数個用意した分の個数(左辺)分だけ代入する個数分を用意しないと"nil"になります。
参考にしたURL:
戻り値が複数ある場合の変数代入
3.ここから学んだこと
変数を複数代入できることは知っていましたが、変数の代入の仕方でもこんなに種類があると思いませんでした。
下記URLから多重代入のことについて載せておきます!参考にしたURL:
Rubyの多重代入あれこれまとめ
- 投稿日:2020-09-21T03:48:13+09:00
ERROR: While executing gem ... (Gem::FilePermissionError)というエラーの対処法
Re:Viewをインストールした際に起きたGemのエラー対処法です。
※Re:Viewのインストールでなくても起きます。$ gem install reviewERROR: While executing gem ... (Gem::FilePermissionError)
You don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory.このようなエラーが出力されます。
gemのインストール先の権限が許可されていない
$ sudo gem install review上記のように
sudo
すればインストール出来ると思いますが、今回はユーザー領域にインストールしたいので、インストール先(GEM_HOME)を明示的に指定してエラーを回避してみます。$ mkdir -p $HOME/rubygems $ export GEM_HOME=$HOME/rubygems $ export PATH=$GEM_HOME/bin:$PATHこの3行を
~/.zshrc
などに記述しておき完了。権限によるトラブルがなくなりました。
参考
- 投稿日:2020-09-21T00:15:54+09:00
Rails ルーティングの基礎
備忘録のために投稿。
環境
ruby 2.7.1
Rails 6.0.3.2Rails の導入は済んでいる前提です。
generate コマンドで必要なファイルを作成
bundle exec rails g controller <controller_name> <method_name> ↓ bundle exec rails g controller home index必要なファイルが作成される。削除したいときは delete コマンド
bundle exec rails d controller home indexroutes.rb
bundle exec rails routes or bundle exec rails routes | grep xxx (絞り込み)ディレクトリ内のルーティングの設定が見れる。
見るべきポイント
URI Pattern Controller#Action /articles/index(.:format) articles#index/articles/index にアクセスすると Controller articles の Action index に飛べる。rails s でサーバ立ち上げて localhost::3000 にアクセスすれば見れる。
Rails のルールとして Controller 名はそのまま View 側の階層構造になっている。例:HomeController(コントローラー名)
View のディレクトリ → home(ディレクトリ名)/index.html.erb(ファイル名)Controller から View への値の渡し方
インスタンス変数を使用する
class HomesController < ApplicationController def index # インスタンス変数 @message = "message" end end変数の前に @ を付ける事で、Controller 内で値をどこでも呼べる。View に変数の値を渡すこともできる。今回の例の場合、インスタンス変数 @message に入った文字列 "message" が渡される。
<h1>Homes#index</h1> <%= @message %>HTML で Ruby を呼び出したい場合、<% %> で使用できる。何か出力したい場合、<%= %> とイコールを付けると出力される。インスタンス変数 @message を渡しているので、文字列 "message" が HTML で表示される。