20200921のRubyに関する記事は19件です。

Ruby×Sinatraで作ったSlackBotをAWS Lambdaで動かしてみる

本記事で目差す構成

Untitled Diagram(1).png

① Slackで特定のアクションを実行する。(※今回はスラッシュコマンド)
② API Gatewayを介してLambdaを起動。
③ Lambdaに配置した関数を実行し情報を返す。

slackbot(2).gif

↑動作イメージとしてはこんな感じ。
今回はとある地域の現在気温を返してくれる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 Lambda

完成形: slack-bot-on-aws-lambda

SlackBotを作成

まず、肝心のSlackBotを作成していく。

ディレクトリを作成

$ mkdir slack-bot-on-aws-lambda
$ cd slack-bot-on-aws-lambda

Rubyのバージョンを指定

# 2.5系なら何でもOK
$ rbenv local 2.5.1 

Sinatraをインストール

$ 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.rb
require '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=4567

Sinatraはデフォルトだとポート番号「4567」で動くため、「localhost:4567」にアクセス。

スクリーンショット 2020-09-21 20.50.33.png

「Hello World!」と表示されれば成功。

天気情報を返すプログラムを実装

今回作るSlackBotの主な機能である天気情報を返すプログラムを実装していく。

OpenWeatherのAPIキーを取得

https://openweathermap.org/
スクリーンショット 2020-09-21 20.54.33.png

上記サイトに会員登録し、APIキーを取得。

スクリーンショット 2020-09-21 20.59.28_censored.jpg

英語で書かれたサービスだが、ある程度は直感的に操作できるので詳しい説明は省略。どうしてもわからなかったらググればいくらでも記事が出てくるはず。

各種Gemをインストール

この先の処理を行う上で必要なGemがいくつかあるため、このタイミングで一気にインストールしておく。

./Gemfile
gem 'faraday'
gem 'rack'
gem 'rack-contrib'
gem 'rubysl-base64'
gem 'slack-ruby-bot'

「bundle install」も忘れずに。

$ bundle install --path vendor/bundle

src/weather.rbを作成

$ mkdir src
$ touch src/weather.rb

src/weather.rbを作成し、次のように記述。

./src/weather.rb
require '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
end

main.rbを編集

./main.rb
require '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
end

SlackBotのトークンを取得する方法については次の記事を参照。

参照: ワークスペースで利用するボットの作成
参照: API トークンの生成と再生成

実際に動作確認

SlackBotをスラッシュコマンドで呼び出すためにいくつか設定しなければならない事がある。

https://api.slack.com/apps/
スクリーンショット 2020-09-21 21.33.08.png
↑のURLにアクセスし、該当のBotを選択。

スクリーンショット 2020-09-21 21.37.35.png
左サイドメニューに「Slash Commands」という項目があるので選択し、「Create New Command」をクリック。
スクリーンショット 2020-09-21 21.40.08.png
各項目を入力していく。

  • 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」をクリック。

スクリーンショット 2020-09-21 21.53.38.png
スラッシュコマンドの作成が終わったら、SlackBotを追加したチャンネルで「/nerima」と打ち込んでみる。上手くいけば画像のようにSlackBotから返答が来る。(設定で画像や名前を変えたりする事も可能。)
スクリーンショット 2020-09-21 22.04.20.png
何か不具合があった場合はターミナルにログが出力されているはずなので、適宜デバッグ。

AWS Lambdaにデプロイ

正常に動作確認できたら、いよいよAWS Lambdaで本番稼働させる。

AWS CLIをインストール

今回は「AWS CLI」と呼ばれるツールを使いながらデプロイしていくので、まだインストールできてないない場合はインストールしておく。

$ brew install awscli

IAMユーザーを作成

デプロイ作業を行うためのIAMユーザーを作成していく。
スクリーンショット 2020-09-21 22.11.08.png
まずは「IAM」→「ポリシー」→「ポリシーの作成」へと進み、JSONタブから以下の文を貼り付ける。
スクリーンショット 2020-09-21 22.13.26.png

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "apigateway:*",
                "cloudformation:*",
                "dynamodb:*",
                "events:*",
                "iam:*",
                "lambda:*",
                "logs:*",
                "route53:*",
                "s3:*"
             ],
            "Resource": [
                "*"
            ]
        }
    ]
}

参照: Minimal Deploy IAM Policy

スクリーンショット 2020-09-21 22.15.47.png
適当にポリシー名や説明を記述し、「ポリシーの作成」をクリック。

スクリーンショット 2020-09-21 22.17.27.png
次に「IAM」→「ユーザー」→「ユーザーの作成」へと進み、適当な名前を付けた後「プログラムによるアクセス」にチェックを入れて次へ進む。

スクリーンショット 2020-09-21 22.17.47.png
「既存のポリシーを直接アタッチ」から先ほど作成した「MinimalDeployIAMPolicy」を選択し、次へ進む。

スクリーンショット 2020-09-21 22.17.59.png

(タグは任意でOK)最後に確認画面が表示されるので、問題無ければ「ユーザーの作成」をクリック。

スクリーンショット 2020-09-21 22.18.13_censored.jpg
すると「アクセスキー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.ru
require 'rack'
require 'rack/contrib'
require_relative './main'

set :root, File.dirname(__FILE__)

run Sinatra::Application
./lambda.rb
require '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
end
template.yaml
AWSTemplateFormatVersion: '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-sample

S3バケットの作成

事前に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」と表示されればデプロイ成功。

スクリーンショット 2020-09-21 22.49.32_censored.jpg

「Lambda」→「関数」と進むと先ほどデプロイした内容が表示されるので、「API Gateway」内に記載されているAPIエンドポイントにアクセス。

スクリーンショット 2020-09-21 22.53.09.png

./main.rb
get '/' do
  'This is SlackBot on AWS Lambda'
end

main.rb内のget '/' リクエストの期待通り「This is SlackBot on AWS Lambda」が返ってくれば正常に動作していると判断してOK。

Slash CommandsのRequest URLを変更

https://api.slack.com/apps/

スクリーンショット 2020-09-21 23.00.35_censored.jpg
再びSlackBotの設定ページにアクセスし、「Slash Commands」からRequest URLを先ほど作成されたエンドポイントに変更する。(https://********.execute-api.ap-northeast-1.amazonaws.com/Prod/webhook」)

スクリーンショット 2020-09-21 23.06.41.png
最後にもう一度、Slackチャンネルで「/nerima」と打ち込み、ちゃんとレスポンスが返ってくればめでたしめでたし。

スクリーンショット 2020-09-21 23.07.45.png
もし上手く行かなかった場合はCloudWatchにログが出力されているはずなので、適宜デバッグ。

あとがき

お疲れ様でした!

今回、簡単なSlackBotをLambdaで動かすというテーマでAWS Lambdaに触れてみました。
大した機能は実装できていないのでLamdaの素晴らしさを全て実感というわけにはいきませんでしたが、上手く使えばかなり便利なサービスだと素人ながら感じています。

難しい操作はしていないため、基本的には手順通りに進めていけば動くはずですが、もし詰まるところがあった場合はコメント欄などで指摘していただけると嬉しいです。

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

Rails 6で認証認可入り掲示板APIを構築する #16 policyの設定

Rails 6で認証認可入り掲示板APIを構築する #15 pundit導入

post_policyの編集

まずはspec/spec_helper.rbを以下のように変更します。
punditの公式にあるように、以下の通り追加すればpundit用のrspecメソッドが使えるようになります。

spec/spec_helper.rb
 RSpec.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
  end

index?と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
  end

not_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.rb
 module 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.rb
class ApplicationPolicy
...

+  private
+
+  def mine?
+    @record.user == @user
+  end

   #
   # scope
   #
  class Scope
...

post_policyに反映してみます。

app/policies/post_policy.rb
   def update?
-    @record.user == @user
+    mine?
   end

   def destroy?
-    @record.user == @user
+    mine?
   end

スッキリしましたね。
これで、今後はuserの関連を持つmodelが自身が所有している場合のみ実行するactionは、policyファイルを作ってmine?メソッドを配置するだけ。超お手軽ですね。

続き

Rails 6で認証認可入り掲示板APIを構築する #17 管理者権限の追加
連載目次へ

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

"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.0

1.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に対応しておらず、プラグインでの対応です。
スクリーンショット 2020-09-05 16.22.48.png

スクリーンショット 2020-09-05 16.26.12.png

早速ですが、公式サイトに記載ある通りプラグインのインストールと実行を行ってみます。
公式サイト:plugin-ruby

私の環境はRails6ですので、yarnでインストールしました。

yarn add --dev prettier @prettier/plugin-ruby`

Rails環境下の一斉実行は下記のコマンドで実行できます。

./node_modules/.bin/prettier --write '**/*.rb'

これで既存のRubyファイルのフォーマットが完了しました。
保存後、エディター、Dockerなど関連ソフトは全て再起動を行ってください。

再起動後に何らかのrbファイルを開いてエディター右下にPrettier,Prettier+のチェックが入っている事を確認ください。
スクリーンショット 2020-09-05 17.01.41.png

その上で、ファイルに空白文字やいくつか改行などを入れてから保存しフォーマットが実行されるか確認してみてください。
うまく実行できればRubyのフォーマット設定は完了です。

2.erbのフォーマット

拡張機能のbeautifyをインストールします。
Prettierがあるのになんで? という意見があると思うのですが、
実際にそれでも構わないのですが、下記のように設定した場合、一部のレイアウトが削除されてerbが分かりづらくなります。
これはerbがhtmlで認識されている為に発生します。

Prettier サンプル例

settings.json
"files.associations": {
   "*.html.erb": "html"
}

Prettier での表示例(ハイライトが消えてわかりづらい)
スクリーンショット 2020-09-05 17.29.34.png


beautifyを拡張機能でインストールしてsettings.jsonで下記の通りに設定を入れます。

  "beautify.language": {
    "html": ["htm", "html", "erb"]
  },

スクリーンショット 2020-09-05 17.30.32.png

これでhtmlのフォーマットがerbにも適応されつつ、erbのハイライトが表示されてわかりやすく作業ができます。

3.JavaScriptのフォーマット

JavaScriptのフォーマット設定はPrettierの設定をjsonに反映するだけでOKです。

settings.json
"[javascript]": {
  "editor.defaultFormatter": "esbenp.prettier-vscode"
},

追記事項

VScodeでのRailsフォーマット環境の一例を紹介しました。
各種設定詳細は割愛しましたので、細かな設定については公式サイト等を参照頂ければと思います。
上記設定でうまく動かない場合は他の拡張機能が邪魔をしている場合がありますので、
拡張機能をいったん無効にして動作確認してみて下さい。

最後に

私の構築した環境がベストな構築方法とは思いませんが、
Rails環境を構築の一例として、構築したフォーマット環境が他の方に少しでも参考になれば幸いです。

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

Ruby on Rails バリデーションまとめ

はじめに

主にRails Tutorialの備忘録として、例などをまとめてみます。
間違い等ありましたらご指摘いただけますと幸いです。

バリデーションとは

オブジェクトをデータベースに保存する前に、オブジェクトの状態を検証することです。
入力された値が無効ではないことを検証します。
例えば、空のデータが保存されないようにしたり、文字数に制限を儲けたりすることができます。

基本的な書き方

基本的な記入方法は以下です。

model
validates :カラム名, ヘルパー

#複数のカラムに適用したい場合
validates :カラム名, :カラム名, :カラム名, ヘルパー

主なバリデーションのヘルパー

presence

指定された属性が空でないことを確認します。

model
validates カラム名, presence: true
model
validates :name, presence: true

#複数のカラムに適用したい場合
validates :name, :login, :email, presence: true

length

属性の値の長さを検証します。多くのオプションがあり、さまざまな長さ制限を指定できます。

model
validates カラム名, length: { 制限 }
model
  validates :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)であり重複していないことを検証します。

model
validates :カラム名, uniqueness: true
model
validates :name, uniqueness: true

acceptance

フォームが送信された時に、チェックボックスがオンになっているかどうかを検証します。
「サービスの利用規約に同意する」にチェックする必要がある場合などに利用されます。

model
validates :カラム名, acceptance: true
model
validates :terms_of_service, acceptance: true

confirmation

複数のフォームで入力された値が完全に一致するかどうかを検証します。
メールアドレスと確認用メールアドレスの値の一致などに利用されます。

model
validates :カラム名, confirmation: true
model
validates :email, confirmation: true

numericality

属性に数値のみが使われていることを検証します。

model
validates カラム名, numericality: true
model
validates :points, numericality: true
model
#整数のみ許可
validates :age, numericality: { only_integer: true }
主なオプション
オプション 概要
:only_integer 整数でなけえればならない
:equal_to 指定された値と等しくなければならない

format

withオプションで与えられた正規表現と属性の値がマッチするかどうかのテストによる検証を行います。

model
validates :カラム名, 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: “カスタムエラーメッセージ” } 
model
validates :name, presence: { message: “必須項目です” }

参考

Active Record バリデーション - Railsガイド v6.0
https://railsguides.jp/active_record_validations.html

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

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.rb
require '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.rb
require "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

(実行時間はループ一回分に換算しています)

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

【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)

外部結合し、全てのデータを取得する。

 参考記事

https://railsguides.jp/active_record_basics.html

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

【Ruby】これで解決。インスタンス変数っていつ使えばいいの?

読んでほしい人

インスタンス変数の意味はなんとなくわかったけど、結局どうローカル変数・インスタンス変数・クラス変数を使い分けるの?
という過去の自分のような人へ。
初学者でもこれさえ読めばわかるように書けていると思います。

結論:スコープです

どこの範囲で使いたい変数なのかを考えるだけ。
つまりスコープ(変数やメソッドを呼び出せる範囲)を考えればよいのです。
以下で解説します。

Rubyで使える変数の種類

1.ローカル変数
2.インスタンス変数
3.クラス変数
4.グローバル変数

それぞれスコープが違います。
もしスコープ外で呼び出そうとしたら下記のように「〇〇〇っていう変数orメソッドなんて定義されてないよ!」とエラーが出ます。
undefined local variable or method `〇〇〇' for main:Object (NameError)

ローカル変数

変数名の前に@も何もつけません。定義したメソッドやクラスの中だけで呼び出せます。

例1 変数morningはmorning_messageメソッドの中だけで使える

local_variable.rb
def 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.rb
class 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.rb
class Color
  def initialize(color)
    @color = color
  end

  def put_color
    puts @color
  end
end

color1 = Color.new("青")
color1.put_color # => 青

例2 initializeメソッド以外でもインスタンス変数を定義

instance_variable.rb
class 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.rb
class 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

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

【Reactで】小説投稿サイトなどに良くある「全○話」みたいなのを表示させる方法

はじめに

今回は小説投稿サイトなどによくある「全○話」を自作アプリにて表示させるのに結構苦労した(3日かかった)ので、戒めとして残しておきたいと思います。

実現したかったこと

  • 用意したのは「シリーズ」というフォルダ的な役割を持つモデルと、「アイテム」というシリーズに複数個格納されるモデルの2つ(1対多の関係)

  • ルートページにて「シリーズ」全件を表示させ、その「シリーズ」が所有する「アイテム」を全て取得し、その総数をカウントさせ「全〜件」という形で表示させたい。

苦労した理由

  • 表示させたいのがルートページだったからです。普通なら各シリーズが所有するアイテムを取得しようとする場合、例えばURLが"/series/104"なら、シリーズのパラメータ(この場合なら104)を取得して、そのパラメータを頼りにアイテムを取得します。

  なので、パラメータが存在しないルートページでどうやって各シリーズのパラメータを取得すりゃええんじゃいとと半ばキレかけながら考えていたわけです(今思えば単純な話でした)

環境・前提等

環境

  • フロントエンド

    • React(v16.8以上)
    • React Hooks(カスタムフックを使う)
    • axios
  • バックエンド

    • Rails(5.2系)

前提

  • CORSの設定、モデル作成などの工程は省略します。
  • PUMAでRails側のローカルホストをデフォルトで3001に指定しています。

Rails側

  • モデル

スクリーンショット 2020-09-21 16.29.12.png

  • コントローラ

    • 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.rb
 Rails.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.rb
class 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

end

React側のコード

// 階層

//src
//  ├ Home.js
//  ├ Series.js
//  ├ ItemCount.js
//  └ useFetch.js

useFetchカスタムフック

src/useFetch.js
import { 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.js
import 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 Home

Seriesコンポーネント

src/Series.js
import 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 Series

ItemCountコンポーネント

src/ItemCount.js
import 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

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

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のバージョンを明示しておいた方が賢明である、という点です。これによって開発環境と本番環境の互換性を最大限に高めることができるので、(バージョンの差異による誤作動やエラーなどが無くなり) オススメです。

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

特異メソッドについて

はじめに

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]

このように特異クラスのスーパークラスは自分のクラスのようです。
メソッド探索は 特異クラス → 自身のクラス → スーパークラス ... の順になっているみたいです。

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

[Rails]flashメッセージの使い方

はじめに

flashとは、ページ遷移した時に簡単なメッセージを一時的に表示させる機能です。
例えば、ユーザーがログインに成功した時に『ログインできました。』と表示させることで、ユーザーが進行具合を確認できるというものです。

目次

1 基本的な書き方
2 flashとflash.nowの違い

基本的な書き方

コントローラーの編集

flashはハッシュのような形で扱います。

flash[:キー名] = “表示さたいメッセージ”

キー名はあらかじめ用意せれているnoticeかalertオプションを用います(自分で好きな名前をつけることも可能です)。

controlle.rb
if @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

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

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.js
function 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.js
function 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秒後以降も同じであるため、重複している処理を防ぐことができます。

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

Rubyの備忘録(キー・バリューの取得)

Rubyの備忘録(キー・バリューの取得)

ハッシュに含まれるキーやバリューの取得には
予め用意されているメソッドがある。

<keysメソッドとvaluesメソッド>
オブジェクト.keys
オブジェクト.values

qiita.rb
puts hash.keys
puts hash.values

これだと、ハッシュに含まれるすべてのキーやバリューが出力される。

{A: "a"}というハッシュがあるとして、
Aというキーを取り出したいときは、Aと対になる値aを()の中に入れる。
逆にaという値を取り出したいときは、aと対になるキーAを()の中に入れる。

qiita.rb
hash = { 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

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

【Ruby on Rails】エラーメッセージの個別表示

目標

error.gif

開発環境

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.rb
validates :name, presence: true
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.まとめ

初学者の私は大量のエラーメッセージを目にするとかなりやる気が削がれてしまいます。エラーメッセージに対しては冷静かつ堅実な対応が必要ですね。エラーに遭遇したときのために、事前に対処法は考えといたほうが良いかもしれないです。

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

入力した引数に対して、複数の戻り値を返したい

【概要】

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の多重代入あれこれまとめ

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

ERROR: While executing gem ... (Gem::FilePermissionError)というエラーの対処法

Re:Viewをインストールした際に起きたGemのエラー対処法です。
※Re:Viewのインストールでなくても起きます。

$ gem install review

ERROR: 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などに記述しておき完了。

権限によるトラブルがなくなりました。

参考

[ruby] gemをユーザー領域にインストールする - 生活。

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

Rails ルーティングの基礎

備忘録のために投稿。

環境

ruby 2.7.1
Rails 6.0.3.2

Rails の導入は済んでいる前提です。

generate コマンドで必要なファイルを作成

bundle exec rails g controller <controller_name> <method_name>

↓

bundle exec rails g controller home index

必要なファイルが作成される。削除したいときは delete コマンド

 bundle exec rails d controller home index

routes.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 で表示される。

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