20190417のRubyに関する記事は28件です。

ハッシュ関数とは

ハッシュ関数とは

プログラミング初心者なりに『ハッシュ関数とは』何かをまとめてみた。

ハッシュ関数とは

一言でいうと『入力されたデータに対して適当な値を返してくれる関数。』

そしてその返ってきた値をハッシュと呼ぶ。

ちなみに関数にいれるものは『引数』呼ぶ。

ハッシュは引数として関数にいれた値をぐちゃぐちゃにしてハッシュとして返してくれる特性があるため、第三者にばれてはいけないパスワードなどの設定になどに使われる。

ちょっと汚い話になるけれど、

ラーメン(引数)を食べたら
それが体(関数)に入って
うんち(ハッシュ)としてでてくる。

うんちを見た人は一体何を食べたのか分からない。

つまりハッシュは、入力されたデータに対して適当な値を返してくれる関数。だと思えばOKだと思う。

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

mrubyでの30日開発日記(3日目)

今日やったこと

  • 数学を学ぶ
  • hiroを読んでみた

やってわかったこと

  • 構造体でのポインタのこととポインタのことが少しわかった。  記事を書いたので読んでみてください!https://qiita.com/eizi2342/items/d568286582c5364c5387
  • 数学は四元数がゲームで使われているのを知ってびっくりした。

今日のコラム

 昨日、ゲームを朝にやったどうなるか、ということを言ったが、結果は成功だった。
 まじで目が覚める。15分ゲームやってみたけどすぐに目が冴えた。本当におすすめ!

Ciao!!

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

声優名で話しかけると出演するゲームを教えてくれるLINE BOTを作ったったー

sample_screen.png

友達登録してくれると喜びます!!

友だち追加 qr_code.png

どんなBOTなの?

声優名で話しかけると出演するゲームを5件まで教えてくれます!!
話しかけ方として以下の3つがあります。

  • 声優名
  • 先月の声優名
  • 来月の声優名

例えば「風音」さんの出演しているゲームを調べたい時はBOTに対して

  • 風音
  • 先月の風音
  • 来月の風音

と話しかけます。

Screenshot_20190412-093431.png

実行タイミングが4月12日(金)なので風音の場合は2019年4月に発売するゲームを教えてくれ、同様に先月の風音の場合は2019年3月で来月の風音の場合は2019年5月の出演するゲームを教えてくれます。

出演するゲームがない場合は出演する予定はありませんと教えてくれます。

声優名以外にも「リスト」と話しかけるとゲームの発売リストページを教えてくれます。
話しかけ方は声優名と同じで以下の3つがあります。

  • リスト
  • 先月のリスト
  • 来月のリスト

sample_list_screen.png

仕組みは?

Ruby(スクレイピングツール)とGASを使用して作成しています。

ざっくりとした図は下記のとおりです。

プログラム詳細.001.jpeg

スクレイピングツール

げっちゅ屋発売日リストとゲームの紹介ページをスクレイピングしその結果をコマンドラインにて表示、絞り込みできるツールになります。

sample.gif
https://github.com/dodonki1223/eroge_release_cmd

このスクレイピングツールでゲームの発売リスト情報を取得しCSVに出力します。

出力したCSVの内容を元にGoogleスプレッドシートへ書き込みを行います。

Googleスプレッドシート

GAS

https://github.com/dodonki1223/eroge_release_bot

GASはLINE BOTと連携してLINE BOTからの入力を受けとり、その内容に合致したものをLINE BOTに返す役割をしています。

作成理由は?

Rubyの勉強のため、Rubyを使って何か作成しようと思い、作ったのがこのLINE BOTです。最初はRubyによるスクレイピングツールだけのつもりだったのですがどうせならLINE BOTも作ってしまえってことで作成しました(笑)
これを作成する前はRuby初心者です。一度もRubyを書いたことありませんでした。

スクレイピングツールの開発について

今回作成したスクレイピングツールのソースはこちら

基本的なRuby構文を書く時に参考にしたこと

公式サイトこそ正義だと思っているので、必ず公式サイトのリファレンスを読んでプログラムを書いていきました。
公式サイトの内容がよくわからない時はブログやQiitaの記事を参考に開発しました。
rubocopというLinterツールのgemを入れ、コードは基本的にコイツに監視してもらい、その他でどう書いたらいいのだろうかというところはCookpadのコーディング規約を参考にしました。

RSpecを書く時に参考にしたこと

今まで働いてきたところではテストコードを書く文化に触れたことがなかったので勉強のために特に力を入れたのがRspecです。
RSpecの独特な記述に苦しまれつつ、書いていました……。慣れてくるといいものですね。
先程でも述べたように公式サイトこそ正義なので必ず公式サイトを読むようにしました。

Better SpecsというRSpecのベスト・プラクティスを教えてくれるサイトを特に参考にしました。ただこのBetter Specsは難しいことはあまり書かれていなくて基本的なことが多い印象です。

応用的な書き方はrubocop内のRSpecを参考にしました。
Mockの作り方やコードの簡略化にすごく参考になりました。

少し古い記事ですが、Mockの考え方は@jnchitoさんの記事を参考にしました。すごくわかりやすくまとめてあり勉強になりました。

スクレイピング

スクレイピングはNokogiriを使用して行いました。スクレイピング関連の記事は探せばいろいろと出てくるのですが、スクレイピングのRSpecコードのサンプルが無くて困りました。
なので今回はスクレイピングのやり方については特に説明しません。興味がある人はGithubのソースを見て下さい。

スクレイピングのRSpecコードで重要だと思ったのはサイトの構造が変わり、値が取得できなくなることだと思ったので、テストは最小限に値が取得できるかどうかをテストしているだけです。

サイト構造が変わったら結局スクレイピングコードを作り直さないといけないと思うので、これぐらいで良いと思いました。

    let(:target_year_month) { '201902' }
    let!(:release_list_scraping) { described_class.new(target_year_month) }

    describe '#scraping' do
      let(:release_list) do
        VCR.use_cassette 'release_list' do
          release_list_scraping.scraping
        end
      end
      let(:first_elemet) { release_list[0] }

      it { expect(first_elemet).to include(:release_date, :title, :introduction_page, :id, :brand_name, :price) }

      it { expect(first_elemet[:release_date]).not_to eq '' }
      it { expect(first_elemet[:title]).not_to eq '' }
      it { expect(first_elemet[:introduction_page]).not_to eq '' }
      it { expect(first_elemet[:id]).not_to eq '' }
      it { expect(first_elemet[:brand_name]).not_to eq '' }
      it { expect(first_elemet[:price]).not_to eq '' }
    end
  end

VCRというRSpecのWebmockを簡単に作成できるgemを使用しています。「VCRを使うとRSpecのWebmockの作成が超絶楽になった! | 酒と涙とRubyとRailsと」のサイトで、ものすごくわかりやすく説明してくれているので、見てみて下さい。

1回目は時間がかかりますが、2回目以降はVCRのおかげで高速でテストが実行できます。

コマンドライン引数

VB.NETでコマンドライン引数を制御するプログラムを作成していた時は自作していたのですが、Rubyだと簡単にコマンドライン引数を制御することができるクラスがあったのですごく助かりました。
この辺を自分で実装するとすごくめんどくさい記憶があるので簡単に実装できて驚きました。下記の記事がものすごく參考になりました。

  # コマンドライン引数クラス
  #   コマンドから受け取ったコマンドライン引数をパースして
  #   プログラムから扱えるようにする機能を提供する
  class CommandLineArg
    attr_accessor :options

    # コンストラクタ
    #   コマンドライン引数を受け取れるキーワードの設定、ヘルプコマンドが実行
    #   された時のメッセージの設定
    #   コマンドライン引数の値を取得するようのHash変数の作成
    def initialize
      # コマンドライン引数の値をセットするHash変数
      @options = {}
      OptionParser.new do |opt|
        # ヘルプコマンドを設定
        opt.on('-h', '--help', 'Show this help') do
          puts opt
          exit
        end

        # げっちゅ屋のrobots.txtの内容を表示するコマンドを設定
        opt.on('--robots', 'Display contents of robots.txt') do
          puts GetchuyaScraping.robots
          exit
        end

        # 値を受け取る系のコマンドライン引数を設定する
        opt.on('-y', '--year_month [YEAR_MONTH]', 'Set Target Year And Month') { |v| set_command_line_arg_value(v, :year_month, '年月') }
        opt.on('-v', '--voice_actor [VOICE_ACTOR]', 'Narrow down by voice actor name') { |v| set_command_line_arg_value(v, :voice_actor, '声優名') }
        opt.on('-t', '--title [TITLE]', 'Filter by title') { |v| set_command_line_arg_value(v, :title, 'タイトル名') }
        opt.on('-b', '--brand_name [BRAND_NAME]', 'Narrow down by brand_name') { |v| set_command_line_arg_value(v, :brand_name, 'ブランド名') }

        # true、falseを受け取るコマンドライン引数を設定する
        # デフォルト値はすべてfalseとし、受け取ったものにはtrueをセットする
        @options[:csv]              = false
        @options[:json]             = false
        @options[:open]             = false
        @options[:spreadsheet]      = false
        @options[:open_spreadsheet] = false
        @options[:clear_cache]      = false
        @options[:simple]           = false
        opt.on('-o', '--open [OPEN]', 'Open game page in browser') { @options[:open] = true }
        opt.on('-c', '--csv [CSV]', 'Create a csv file') { @options[:csv] = true }
        opt.on('-j', '--json [JSON]', 'Create a json file') { @options[:json] = true }
        opt.on('-s', '--spreadsheet [SPREADSHEET]', 'Write to spreadsheet from CSV') { @options[:spreadsheet] = true }
        opt.on('--open_spreadsheet [OPEN_SPREADSHEET]', 'Open spreadsheet page in browser') { @options[:open_spreadsheet] = true }
        opt.on('--clear_cache [CLEAR_CACHE]', 'Clear the cache') { @options[:clear_cache] = true }
        opt.on('--simple [SIMPLE]', 'Display results in a simplified way') { @options[:simple] = true }

        # コマンドラインをparseする
        opt.parse!(ARGV)
      end
    end

    # 対象のコマンドライン引数が存在するか?
    def has?(name)
      @options.include?(name)
    end

    # 対象のコマンドライン引数の値を取得する
    #   対象のコマンドライン引数の値が存在しない場合は空文字を返す
    def get(name)
      return '' unless has?(name)

      @options[name]
    end

    private

    # コマンドライン引数をインスタンス変数にセットする
    #   もし対象のコマンドライン引数がnilの時はメッセージを表示して処理を終了する
    def set_command_line_arg_value(value, key, param_name)
      if value.nil?
        puts "#{param_name}のパラメータを指定して下さい"
        exit
      end
      # 配列かどうかを取得し、配列の時は配列をセットそうでない時は値をそのままセットする
      is_array = value.split(',').count > 1
      set_value = is_array ? value.split(',') : value
      @options[key] = set_value
    end
  end

Googleスプレッドシートの操作

Googleスプレッドシートのシートを削除したり、新規に作成したりするのにgoogle-drive-rubyというgemを使用しました。
使い方についてはドキュメント通りなのですが、ドキュメント以外のことをやろうと思った時、あまりサンプルがなかったのでGithubのソースを見てどんなことができるのか確認しながら作成しました。

# コンストラクタ
#   IDを引数で受け取り、もしIDが存在しなかった場合はGoogleスプレッドシートが見つかりません例外が
#   発生する
#   ※必ず存在するGoogleスプレッドシートがあること前提です
def initialize(spreadsheet_id)
  # jsonファイルのconfigファイルからGoogleDrive::Sessionを作成する
  # https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.mdに
  # google_drive_config.jsonファイル作成方法が書かれています
  @session = GoogleDrive::Session.from_config(CONFIG_FILE_PATH)
  begin
    @spreadsheet = @session.spreadsheet_by_key(spreadsheet_id)
  rescue Google::Apis::ClientError => e
    puts "指定されたIDのスプレッドシートが見つかりませんでした\n#{e.message}(#{e.class})"
    raise
  end
end
# @spreadsheetは「GoogleDrive::Spreadsheet」です

# Googleスプレッドシートのワークシートをタイトルから削除する
#   ワークシートが見つからなかった場合はメッセージを表示
def delete_worksheet_by_title(title)
  # 削除対象のワークシートを取得
  target_worksheet = @spreadsheet.worksheet_by_title(title)

  # 削除対象のワークシートが見つからなかった場合はメッセージを表示して処理を終了する
  if target_worksheet.nil?
    puts "ワークシート名が「#{title}」のワークシートが見つかりませんでした"
    puts 'ワークシートの削除が出来ませんでした'
    return
  end

  # ワークシートの削除処理を実行
  target_worksheet.delete
end
# @spreadsheetは「GoogleDrive::Spreadsheet」です

# Googleスプレッドシートのワークシートをタイトルから取得する
#   ワークシートが存在しない時は作成したワークシートを@worksheetにセットする存在する時はそのワー
#   クシートを@worksheetにセットする
def get_worksheet_by_title_not_exist_create(title)
  @worksheet = @spreadsheet.worksheet_by_title(title).nil? ? @spreadsheet.add_worksheet(title) : @spreadsheet.worksheet_by_title(title)
  @worksheet
end
@worksheetは「GoogleDrive::Worksheet」です

# GoogleスプレッドシートへCSVファイルから書き込みを行なう
def write_by_csv(file_path)
  begin
    # CSVファイルの読み込み
    csv_data = CSV.read(file_path)
  rescue StandardError => e
    # 例外メッセージとバックトレースを表示して処理を終了する
    puts "CSVファイルが見つかりませんでした\n#{e.message}(#{e.class})"
    raise
  end
  # CSVファイルからデータを取得し、スプレッドシートへ書き込む
  csv_data.each_with_index do |data, row|
    data.each_with_index do |value, cell|
    # google-drive-rubyの仕様で1行目は1から、1セル目は1からなので行とセルにそれぞれ+1する
    @worksheet[row + 1, cell + 1] = value
    end
  end
  @worksheet.save
end

google-drive-rubyのコードのRSpecはMockを多用して書きました。
量が多いのでリンクだけ貼っておきます。興味がある人は読んで見て下さい。

LINE BOTの開発について

LINE BOT

LINEの社員さんが書かれているQiitaの記事を參考にLINEの設定を行いました。
Node.jsのプログラム開発以外はこちらを參考にしました。

LINE BOTとGASとの連携

下記記事を參考にしました。

LINE BOTとGASの連携は本当に簡単です。1時間もあれば簡単に連携することができました。

LINE Developersの設定でWebhook送信利用するにし忘れることがあります。
利用するにしてないとLINEから文字を入力してもうんともすんとも反応しないので必ず利用するになっているか確認して下さい。

LineImage

GASについて

今回のGASの役割はLINEからの入力を受け取り、受け取った内容でスプレッドシート内を検索し合致した内容をLINEに返すことをしています。

今回作成したGASのソースはこちら

LINEからPOSTされた時に実行される処理

GASをWEBアプリケーションとして公開しPOSTを受け取る時はdoPost(e)を定義しなくてはいけません。その辺のことはWeb Apps  |  Apps Script  |  Google Developersに書いてあるので読んでみると良いかもしれないです。
つまりLINE BOTを作るだけならdoPost(e)メソッドだけ用意してあげればOKです。
今回作成したBOTのdoPost(e)メソッドの中身を紹介していきます。

function doPost(e) {
  // LineBotからPostされたデータを取得
  // Webhookイベントオブジェクトの返す値について
  // →https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objects
  var replyToken  = JSON.parse(e.postData.contents).events[0].replyToken,  // WebHookで受信した応答用Token
      userMessage = JSON.parse(e.postData.contents).events[0].message.text; // ユーザーのメッセージを取得(声優名)

  // 対象の月を取得する
  var targetMonth = getTargetMonth(userMessage);

  // 対象のシートを取得
  var yearMonth = getYearMonth(targetMonth),
      sheet     = getSheet(yearMonth);

  // 声優名を取得する
  var voiceActorName = getVoiceActorName(userMessage)

  // 声優名から対象のスプレッドシートの行を取得
  var foundRows = getRowsByVoiceActor(sheet, voiceActorName);

  // LineにPostする
  if (voiceActorName == 'リスト') {
    var year  = yearMonth.slice(0,4),
        month = yearMonth.slice(4);
    UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createListPagePostMessage(year, month)));
  } else {
    if (foundRows.length == 0) {
      UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createNotExistsPostMessage(targetMonth, voiceActorName)));
    } else {
      UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createPostMessages(sheet, foundRows)));
    }
  }
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

もう少し細かく確認していきましょう。

LINEからPOSTされた情報を受け取る

LINEからは下記のような感じでPOSTデータが返ってきます。
詳しくはMessaging APIリファレンスを読んで見て下さい。

{
  "destination": "xxxxxxxxxx", 
  "events": [
    {
      "replyToken": "0f3779fba3b349968c5d07db31eab56f",
      "type": "message",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      },
      "message": {
        "id": "325708",
        "type": "text",
        "text": "Hello, world"
      }
    },
    {
      "replyToken": "8cf9239d56244f4197887e939187e19e",
      "type": "follow",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      }
    }
  ]
}

LINEからの入力情報を取得する

  // LineBotからPostされたデータを取得
  var replyToken  = JSON.parse(e.postData.contents).events[0].replyToken,  // WebHookで受信した応答用Token
      userMessage = JSON.parse(e.postData.contents).events[0].message.text; // ユーザーのメッセージを取得(声優名)

replyTokenはイベントへの応答に使用するトークンです。
userMessageはLINEから入力された声優名が入ります。

シートを取得

Googleスプレッドシートには年月の単位でシートができています。
シート名で取得できるようにしています。

名称未設定.png

シートの取得方法

  // 対象の月を取得する
  // 先月、来月などの文字から対象月を取得する
  // 2019年4月16日に実行した場合は下記のように判断する
  // 声優花子   :今月 → 「 0」で返ってくる
  // 先月の声優花子:先月 → 「-1」で返ってくる
  // 来月の声優花子:来月 → 「+1」で返ってくる
  var targetMonth = getTargetMonth(userMessage);

  // 対象のシートを取得
  // 対象の月から年月を取得し、対象のシートを取得する
  // 2019年4月16日に実行した場合は下記のようにシートを判断する
  // 声優花子   :201904
  // 先月の声優花子:201903
  // 来月の声優花子:201905
  // yearMonthには「201904」、「201903」、「201905」などの文字列が取得されます
  var yearMonth = getYearMonth(targetMonth),
      sheet     = getSheet(yearMonth);

年月の情報を取得するメソッド群

// 検索する対象月の値定数
var targetMonth = {
  LastMonth          : -1,
  NextMonth          : 1,
  CurrentMonth       : 0,
  LastMonthString    : '先月の',
  NextMonthString    : '来月の',
  CurrentMonthString : '今月の'
}

// 対象の月情報を取得する
// 文字列に「先月の」、「来月の」などの文字列でどの月が対象か判断する
// 「0」か「1」か「-1」を返す
function getTargetMonth(message) {
  if (message.indexOf(targetMonth.LastMonthString) != -1) {
    return targetMonth.LastMonth
  } else if (message.indexOf(targetMonth.NextMonthString) != -1) {
    return targetMonth.NextMonth;
  } else {
    return targetMonth.CurrentMonth;
  }
}

// 年月の文字列を返す
// addMonthの値はgetTargetMonthで取得した値が来る想定です
// YYYYMMDD形式の文字をを返す
// YYYYMMDD形式がシート名となっているため
function getYearMonth(addMonth) {
  var date = new Date();

  // addMonthの内容により◯ヶ月前、◯ヶ月後の情報をセットする
  date.setMonth(date.getMonth() + addMonth);

  // 月は-1で取得されるため、+1する
  var year  = date.getFullYear(), 
      month = date.getMonth() + 1;

  // 月が1桁時は前0を付加して年月の文字列を返す
  return year + ('0' + month).slice(-2);
}

シートを取得するメソッド

// シート名から対象のシートオブジェクトを取得しています
// SheetNameには「201904」、「201903」、「201905」がくる想定です
function getSheet(SheetName) {
  var ss    = SpreadsheetApp.getActiveSpreadsheet(),
      sheet = ss.getSheetByName(SheetName);
  return sheet;
}

声優名から合致するデータを取得する

声優名から対象の行を抽出する

  // 声優名を取得する
  var voiceActorName = getVoiceActorName(userMessage)

  // 声優名から対象のスプレッドシートの行を取得
  var foundRows = getRowsByVoiceActor(sheet, voiceActorName);

声優名を取得するメソッド

// LINE BOTから受け取った文字列から「先月の」、「来月の」の文字列を排除した
// 文字列を受け取る
// 下記のように感じなります
// 声優花子   :声優花子
// 先月の声優花子:声優花子
// 来月の声優花子:声優花子
function getVoiceActorName(message) {
  replaceMessage = message.replace(targetMonth.LastMonthString, '');
  return replaceMessage.replace(targetMonth.NextMonthString, '');
}

声優名から対象の行を取得するメソッド

// 声優名から対象の行を取得するメソッド
function getRowsByVoiceActor(sheet, voiceActorName) {
  // 声優名列はI列なのでI列を対象に最終行までのRangeオブジェクトを取得します
  var startRow  = 2,
      searchRangeArea = 'I' + startRow + ':I' + sheet.getLastRow(),
      range     = sheet.getRange(searchRangeArea),
      foundRows = [];

  // Rangeのスタートが2行目のため、Rangeの行数+1する
  var searchMaxRow = range.getNumRows() + 1;

  // 声優名列から対象の声優が出演しているか検索し結果を配列にセット
  // 1行ずつ確認していき、対象の声優の文字列がある時は出演していると判断する
  // 声優名列には「声優花子、声優ゴリラ、声優キリン」のような文字列にLINE BOTからの文字列が
  // 含まれているか判断しているだけなので結構ゆるい感じで判断しています 
  for(var row = startRow; row <= searchMaxRow; row++) {
    cellValue = sheet.getRange("I" + row).getValue();
    if (cellValue.indexOf(voiceActorName) != -1) 
      foundRows.push(row);
  }

  // APIの制限で5件までしか送信できないため、最初から5件のみを取り出す
  // リクエストボディのところに5件までと書かれています
  // https://developers.line.biz/ja/reference/messaging-api/#anchor-6640e4a392930e46edb1c15c1d6817ee2356f75e
  return foundRows.slice(0, 5);
}

LINEにPOSTする

UrlFetchAppを使用してLINEにPOSTします。
詳しくはClass UrlFetchApp  |  Apps Script  |  Google Developersを確認して下さい。

リストとそうでない時で処理を切り分けています。
さらに入力された声優名の行が存在する時としない時で切り分けています。

  // config.LinePostUrlにはLINEにPOSTするURLが入っています。
  // 「https://api.line.me/v2/bot/message/reply」になります。

  // LineにPostする
  if (voiceActorName == 'リスト') {
    // 年月の文字列から年と月を切り分ける
    var year  = yearMonth.slice(0,4),
        month = yearMonth.slice(4);
    UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createListPagePostMessage(year, month)));
  } else {
    if (foundRows.length == 0) {
      UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createNotExistsPostMessage(targetMonth, voiceActorName)));
    } else {
      UrlFetchApp.fetch(config.LinePostUrl, createRequest(replyToken, createPostMessages(sheet, foundRows)));
    }
  }
// POSTのリクエストを作成している箇所です
// callbackにはLINEのメッセージオブジェクトの配列が入ります
// メッセージオブジェクトについては
// https://developers.line.biz/ja/reference/messaging-api/#message-objects
// を読んで下さい
function createRequest(replyToken, callback) {
  return {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + config.LineAccessToken,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': callback,
    }),
  }
}

メッセージオブジェクトを作成している箇所です
メッセージオブジェクトについてはこちらを読んで下さい

// 対象の声優がゲームに出演している時
function createPostMessages(sheet, rows) {
  // 行数分の配列を作成します
  return rows.map(function(row) {
    return {
      'type': 'text',
      'text': postMessage(sheet, row),
    }
  });
}

 // 対象の声優がゲームに出演していなかった時
function createNotExistsPostMessage(month, voiceActorName) {
  return [{
    'type': 'text',
    'text': notExistPostMessage(month, voiceActorName),
  }]
}

// リストページを返す時
function createListPagePostMessage(year, month) {
  return [{
    'type': 'text',
    'text': listPagePostMessage(year, month),
  }]
}

メッセージ作成メソッド群

// 対象の声優がゲームに出演している時のメッセージの時
function postMessage(sheet, row) {
  // 対象の行データを全て取得する
  var rowValues        = sheet.getRange(row, 1, 1, maxColumnsCount).getValues()[0];

  // メッセージに表示するための情報を取得します
  var releaseDate      = Utilities.formatDate(rowValues[columns.ReleaseDate], "JST", "yyyy/MM/dd"),
      title            = rowValues[columns.Title],
      price            = rowValues[columns.Price],
      introductionPage = rowValues[columns.IntroductionPage],
      brandPage        = rowValues[columns.BrandPage];

  var message = releaseDate + '\n' +
                title + '\n' +
                price + '\n' +
                introductionPage + '\n' +
                '\n' +
                brandPage;

  return message;
}

// 対象の声優がゲームに出演していなかった時のメッセージの時
function notExistPostMessage(month, voiceActorName) {
  var message = '';

  if (month == targetMonth.LastMonth) {
    message = targetMonth.LastMonthString + voiceActorName;
  } else if (month == targetMonth.NextMonth) {
    message = targetMonth.NextMonthString + voiceActorName;
  } else {
    message = targetMonth.CurrentMonthString + voiceActorName;
  }

  message = message + 'はゲームに出演する予定はありません'

  return message;
}


// リストページのメッセージの時
function listPagePostMessage(year, month) {
  var message = year + '年' + month + '月の発売リストページです\n' + 
               'http://www.getchu.com/all/price.html?genre=pc_soft&year=' + year + 
               '&month=' + month + 
               '&gage=&gall=all';
  return message;
}

Slackに通知する

LINE BOTだけのつもりだったのですが、スクレイピングツールで書き込んだ内容が正しいのかどうかを判断するため、Slackにも通知されるようにしました。GASのいいところは定期実行が簡単に指定できるので、決まった時間にSlackに通知させることは容易です。

Slack通知

Slack通知を行うためのコードが下記になります。Slack通知でめんどくさいのがAttachementsの作成です。
逆に言うとAttachementsを頑張って作成するといい感じでSlackに通知されるようになります。Attachementsは通知される時のメッセージのデザインを細かく設定できるものです。

// 発売リスト一覧のメッセージをSlackに送信する
function releaseListSendMessage() {
  // 送信するメッセージ情報を作成する
  var yearMonth = getYearMonth(0),
      sheet     = getSheet(yearMonth),
      foundRows = getAllRows(sheet);
  var attachments = (function(sheet, rows) {
    // 対象のデータを全て取得しそのデータからAttachementを作成する
    var rowsCount = rows[rows.length - 1] - 1,
        values    = sheet.getRange(2, 1, rowsCount, maxColumnsCount).getValues();

    Logger.log(values);

    return values.map(function(value) {
      var brandName        = value[columns.BrandName],
          title            = value[columns.Title],
          introductionPage = value[columns.IntroductionPage],
          releaseDate      = Utilities.formatDate(value[columns.ReleaseDate],"JST","yyyy/MM/dd"),
          price            = value[columns.Price],
          voiceActors      = value[columns.VoiceActor],
          packageImage     = value[columns.PackageImage];
      return createAttachement(brandName, title, introductionPage, releaseDate, price, voiceActors, packageImage);
    });
  }(sheet, foundRows));

  // メッセージを作成する
  var year    = yearMonth.slice(0,4),
      month   = yearMonth.slice(4),
      message = year + '年' + month + '月の発売リスト';

  // Slackにメッセージを送信する 
  sendMessage(message, 
              config.SlackPostChannel, 
              '発売リストくん', 
              config.SlackPostUserIcon, 
              attachments);
}

// Attachementを作成する
function createAttachement(brandName, title, introductionPage, releaseDate, price, voiceActors, image) {
  return {
      "author_name": brandName,
      "color": "#a8bdff",
      "title": title,
      "title_link": introductionPage,
      "text": releaseDate + '\n' + 
              price + '\n' + 
              voiceActors,
      "image_url": image
  }
}

// Slackのあるチャンネルにメッセージを送信する
function sendMessage(message, channel, username, iconUrl, attachments) {
  var payload = {
    channel: channel,
    text: message,
    username: username,
    icon_url: iconUrl,
    attachments: attachments,
  };

  var option = {
    'method': 'post',
    'payload': JSON.stringify(payload),
    'contentType': 'application/x-www-form-urlencoded; charset=utf-8',
    'muteHttpExceptions': true
  };

  // Slackにメッセージを送信
  var response = UrlFetchApp.fetch(config.SlackWebHookUrl, option);
}

設定方法としては、スクリプトエディタからトリガーを設定するだけです。

sample_trigger_button.png

トリガー設定画面
設定は関数単位で指定できるのでreleaseListSendMessageを設定します。

torigger_scrreen

sample_trigger_page.png

この設定で1週間ごと、毎週金曜日にSlackに通知されるようになります。

まとめ

Googleスプレッドシートへの書き込みがまだ手動なので今後はここを自動化したいと思っています。

開発していて一番時間がかかったのはRubyのRSpecです。1ヶ月以上戦っていた気がします。LINE BOTとGASに関しては触ったことがなかったのですが10時間ぐらいでできました。
10時間ぐらいなので休みの日をかけてLINE BOTを余裕で作成できるレベルです。

皆さんも気軽にLINE BOTを作成してみるのものいいかもしれません!

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

Railsでテーブルのカラムを確認したい時

Railsにてカラムを確認したい時は、コンソールからモデル名を入力すれば確認できる様子

2.4.1 :017 > User
 => User(id: integer, organization_id: integer, login_id: string, password_digest: string, first_logged_in_at: datetime, last_logged_in_at: datetime, created_at: datetime, updated_at: datetime, deleted_at: datetime) 

補足:Hoge (call 'Hoge.connection' to establish a connection)が出た時

=> Hoge (call 'Hoge.connection' to establish a connection)

上記のように表示されたときは
ActiveRecord::Base.clear_cache!をコンソールに入力すれば良いらしい。

http://nyaahara.hatenablog.com/entry/2015/02/19/234513

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

rails でherokuにデプロイできない、際のwarning: constant ::Fixnum is deprecated remote: warning: constant ::Bignum is deprecated remote: rake aborted! remote: SystemStackError: stack level too deep の対処法

問題の内容

Bundle completed (47.31s) remote: Cleaning up the bundler cache. 
remote: -----> nstalling node-v10.14.1-linux-x64 remote: -----> 
Detecting rake tasks remote: ----->Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: /tmp/build_14414a0b13dd75a9888c3698db467726/vendor/bundle/ruby/2.5.0/gems/activesupport-4.2.5/lib/active_support/core_ext/numeric/conversions.rb:121: 

warning: constant ::Fixnum is deprecated remote: 

/tmp/build_14414a0b13dd75a9888c3698db467726/vendor/bundle/ruby/2.5.0/gems/activesupport-4.2.5/lib/active_support/core_ext/numeric/conversions.rb:121: 

warning: constant ::Bignum is deprecated remote: rake aborted! remote: SystemStackError: stack level too deep
 /tmp/build_611b721386924ba6fb37a7ef7af1bee4/vendor/bundle/ruby/2.5.0/gems/activesupport-4.2.5/lib/active_support/core_ext/numeric/conversions.rb:131:in `block (2 levels) in <class:Numeric>'
~ 以下、延々と同じエラーメッセージが大量に出力 ~

問題が発生した環境
・Rails 4.2.5

調べた結果

どうやらRails4.2.5のソースコードの中にFixnum?とBignum?を使っている箇所があることがエラーの原因であるみたい。なんやそれ笑

Qiitaで参考にした解説
https://qiita.com/jkr_2255/items/647c427d2c2f7892fa93

ということで、Railsを4.2.5から4.2.8に上げてみることにしました。

Gemfileのrailsのバージョン指定を次のように変更して保存し、bundle updateを実行します。

source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.8'

その後、改めてgit push heroku masterを行うと無事、デプロイが成功するようになりました。

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

rails でherokuにデプロイできない。際のwarning: constant ::Fixnum is deprecated remote: warning: constant ::Bignum is deprecated remote: rake aborted! remote: SystemStackError: stack level too deep の対処法

問題の内容

Bundle completed (47.31s) remote: Cleaning up the bundler cache. 
remote: -----> nstalling node-v10.14.1-linux-x64 remote: -----> 
Detecting rake tasks remote: ----->Preparing app for Rails asset pipeline remote: Running: rake assets:precompile remote: /tmp/build_14414a0b13dd75a9888c3698db467726/vendor/bundle/ruby/2.5.0/gems/activesupport-4.2.5/lib/active_support/core_ext/numeric/conversions.rb:121: 

warning: constant ::Fixnum is deprecated remote: 

/tmp/build_14414a0b13dd75a9888c3698db467726/vendor/bundle/ruby/2.5.0/gems/activesupport-4.2.5/lib/active_support/core_ext/numeric/conversions.rb:121: 

warning: constant ::Bignum is deprecated remote: rake aborted! remote: SystemStackError: stack level too deep
 /tmp/build_611b721386924ba6fb37a7ef7af1bee4/vendor/bundle/ruby/2.5.0/gems/activesupport-4.2.5/lib/active_support/core_ext/numeric/conversions.rb:131:in `block (2 levels) in <class:Numeric>'
~ 以下、延々と同じエラーメッセージが大量に出力 ~

問題が発生した環境
・Rails 4.2.5

調べた結果

どうやらRails4.2.5のソースコードの中にFixnum?とBignum?を使っている箇所があることがエラーの原因であるみたい。なんやそれ笑

Qiitaで参考にした解説
https://qiita.com/jkr_2255/items/647c427d2c2f7892fa93

ということで、Railsを4.2.5から4.2.8に上げてみることにしました。

Gemfileのrailsのバージョン指定を次のように変更して保存し、bundle updateを実行します。

source 'https://rubygems.org'


# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.2.8'

その後、改めてgit push heroku masterを行うと無事、デプロイが成功するようになりました。

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

【Rails】ローカル開発環境で他の端末からrails serverに接続する方法

IPアドレス確認

ターミナルでifconfig コマンドを入力して、wi-fiで接続しているインターフェイスのIPアドレスを確認する。

$ ifconfig
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    nd6 options=201<PERFORMNUD,DAD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
XHC0: flags=0<> mtu 0
XHC20: flags=0<> mtu 0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether xxxxxxxxxxxxxxxxxxx
    inet6 fe80::a5:5017:3c48:df62%en0 prefixlen 64 secured scopeid 0x6 
    inet 192.168.11.10 netmask 0xffffff00 broadcast 192.168.11.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active

en0: を見ると 192.168.11.10 であるのが分かる。

serverを立ち上げる。

serverのPCはrails serverを立ち上げます。この時-b オプションが必要。

rails s -b 0.0.0.0

外部端末から接続

rails s したPCと同じWi-Fi(正確には同一のSSID)に接続し、
ブラウザのアドレスバーにserverのIPアドレスと3000番ポート番号を指定して入力する。
今回の場合、サーバーのPCのIPアドレスが192.168.11.10なので、192.168.11.:3000
と入力する。

スクリーンショット 2019-04-17 22.00.18.png

これでherokuなどにデプロイしなくても、スマホから画面確認などが出来ます。
また、httpなのでwiresharkなどでパケットキャプチャーすれば中身が見れます。

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

【Rails】development環境で外部端末からserverに接続する方法

IPアドレス確認

ターミナルでifconfig コマンドを入力して、wi-fiで接続しているインターフェイスのIPアドレスを確認する。

$ ifconfig
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    nd6 options=201<PERFORMNUD,DAD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
XHC0: flags=0<> mtu 0
XHC20: flags=0<> mtu 0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether xxxxxxxxxxxxxxxxxxx
    inet6 fe80::a5:5017:3c48:df62%en0 prefixlen 64 secured scopeid 0x6 
    inet 192.168.11.10 netmask 0xffffff00 broadcast 192.168.11.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active

en0: を見ると 192.168.11.10 であるのが分かる。

serverを立ち上げる。

serverのPCはrails serverを立ち上げます。この時-b オプションが必要。

rails s -b 0.0.0.0

外部端末から接続

rails s したPCと同じWi-Fi(正確には同一のSSID)に接続し、
ブラウザのアドレスバーにserverのIPアドレスと3000番ポート番号を指定して入力する。
今回の場合、サーバーのPCのIPアドレスが192.168.11.10なので、192.168.11.:3000
と入力する。

スクリーンショット 2019-04-17 22.00.18.png

これでherokuなどにデプロイしなくても、スマホから画面確認などが出来ます。
また、httpなのでwiresharkなどでパケットキャプチャーすれば中身が見れます。

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

Railsの部分テンプレ(partial)をラッピングして、collection変数を渡す

TL;DR

_item.html.slim
h3 Item Name: #{item.name}
p Item Index: #{item_counter}
_wrapped_item.html.slim
h2 Item Wrapper
.item-area
  = render partial: 'item',\
    locals: { item: wrapped_item, item_counter: wrapped_item_counter }
index.html.slim
h1 Naked Items
.naked-items = render partial: 'item', collection: @items

h1 Wrapped Items
.wrapped-items = render partial: 'wrapped_item', collection: @items

はじめに

パーシャルをrenderする際、<%= render partial: 'item', collection: @items %>を使うと、.each文より見通しが良くなるだけでなく、性能も向上されるとされています。
しかし、パーシャルにそれぞれラッピングやデコレーションをしたい場合、どうすれば良いのでしょう。

発見

_item.html.slim
h3 Item Name: #{item.name}
p Item Index: #{item_counter}
_wrapped_item.html.slim
h2 Item Wrapper
.item-area = render partial: 'item'
index.html.slim
h1 Naked Items
.naked-items = render partial: 'item', collection: @items

h1 Wrapped Items
.wrapped-items = render partial: 'wrapped_item', collection: @items

上記の構文のように、そのまま_item.html.slimにラッピングを当てて、renderingしようとする時、Template::Errorが発生し、変数itemがないと言われました。

ActionView::Template::Error (undefined local variable or method `item' for #<#<Class:0x00007f8d8fa105d0>:0x00007f8da6f657a8>:
    1: h3 Item Name: #{item.name}
    2: p Item Index: #{item_counter}

だったら変数itemを手動で渡せば良いのでは?と思って、変数wrapped_itemitemとして渡しました。もちろん、Indexも必要な時は、#{variable_name}_countも伝播しなければいけません。

_wrapped_item.html.slim
h2 Item Wrapper
.item-area
  = render partial: 'item',\
    locals: { item: wrapped_item, item_counter: wrapped_item_counter }

参考

https://qiita.com/otakumesi/items/74a75cd6ebff7eba5c63

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

Rails6 のちょい足しな新機能を試す4 (Date#before? Date#after? 編)

はじめに

Rails 6 に追加されそうな新機能を試す第3段。 Date#before? Date#after? 編です。
記載時点では、Rails は 6.0.0.beta3 です。 gem install rails --prerelease でインストールできます。

試してみる

今回は、rails の projectを作らなくても試せるので、直接、ruby (irb) から試してみましょう。

$ irb -ractive_support/core_ext

# 今日は昨日より後?
irb(main):001:0> Date.today.after?(Date.today.yesterday)
=> true

# 今日は明日より後?
irb(main):002:0> Date.today.after?(Date.today.tomorrow)
=> false

# 今日は昨日より前?
irb(main):003:0> Date.today.before?(Date.today.yesterday)
=> false

# 今日は明日より前?
irb(main):004:0> Date.today.before?(Date.today.tomorrow)
=> true

# 今日は今日より後?
irb(main):005:0> Date.today.after?(Date.today)
=> false

# 今日は今日より前?
irb(main):006:0> Date.today.before?(Date.today)
=> false

DateTime, Time, TimeWithZone にも同様に before? と after? が追加されています。

これ、わかりやすいんですかね。英語ペラペラの人にとっては自然なのかもですが、そうでない人には、ちょっと微妙かもと思ったり思わなかったり。以下みたいな読み違いをしそうな気がしないでもない...

# 今日の後は昨日? => この解釈が間違い。
irb(main):001:0> Date.today.after?(Date.today.yesterday)
=> true

このメソッドは、 :>after? の <: が before? の aliasとして定義されています。と書こうとしたのですが、どうやらそうではなかったようです。

irb(main):015:0> Date.today.method(:before?).original_name
=> :before?
irb(main):016:0> Date.today.method(:after?).original_name
=> :after?

アレッと思ってソースを調べてみました。

irb(main):018:0> Date.today.method(:before?).source_location
=> ["/usr/local/bundle/gems/activesupport-6.0.0.beta3/lib/active_support/core_ext/date_and_time/calculations.rb", 64]
irb(main):019:0> Date.today.method(:after?).source_location
=> ["/usr/local/bundle/gems/activesupport-6.0.0.beta3/lib/active_support/core_ext/date_and_time/calculations.rb", 69]

実際のコードはこんな感じです。

calclulation.rb
    # Returns true if the date/time falls before <tt>date_or_time</tt>.
    def before?(date_or_time)
      self < date_or_time
    end

    # Returns true if the date/time falls after <tt>date_or_time</tt>.
    def after?(date_or_time)
      self > date_or_time
    end

調べてみたら、alias で定義していたものを refactoring してました。

元のPRが Add before? and after? methods to date and time classes で、
refactoring の PR が Move implementation of before? and after? to DateAndTime::Calculations です。

参考情報

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

インフラエンジニアだけどLINE Botを作りたい

はじめに

お仕事的にはインフラエンジニアですが、昨今の技術の進歩からインフラエンジニアいらないんじゃない的なことを感じたり感じなかったりなので、インフラエンジニアでもある程度コードかけないとまずいとは思ってるけど、じゃいざ何作るの?って段階になるとなかなかアイデアで出ない人は多いのではないのでしょうか。

そんな人は簡単なLINE Botを作ってみるとこから初めて見たらいかがでしょうか?という記事です。

環境

何かしらでBotサーバーを用意しなければなりません。
選択肢としては、GASやHerokuなどが思いつくと思いますが、普段私がAWSしかいじってないのでAPI Gateway + Lambda(Ruby)をserverless frameworkでデプロイします。

普段AWSをいじってる人はこの構成でやることを是非オススメします

手順

ここからはLINE Botが定型文を返すまでの手順となります

1. LINE@アカウントの取得

まずは以下のリンクからLINE@アカウント(一般アカウント)を取得する必要があります。

https://entry-at.line.me/

一般アカウントとは?

「一般アカウント」は審査なしで作成できるアカウントです。
一般アカウントの作成後に認証済みアカウントを申し込むことも可能です。
ご利用可能なプランはお住まいの国によって異なります。

2. チャネルの作成

LINE@アカウントを作成したらチャネルを作成する必要があります。

チャネルとは?

チャネルは、LINEプラットフォームが提供する機能を、プロバイダーが開発するサービスで利用するための通信路です。LINEプラットフォームを利用するには、チャネルを作成し、サービスをチャネルに関連付けます。チャネルを作成するには、名前、説明文、およびアイコン画像が必要です。チャネルを作成すると、固有のチャネルIDが識別用に発行されます。

https://developers.line.biz/ja/docs/messaging-api/getting-started/

チャネル作成の詳細な手順は以下の通りです。

2-1. LINEアカウントでLINE Developersコンソールにログイン

2-2. 開発者として登録する(初回ログイン時のみ)

2-3. 新規プロバイダーを作成する

2-4. チャネルの作成

このとき以下のどちらかのプランを選ぶ必要がありますが、お遊びの範囲であればDeveloper Trialを選択してください。

Developer Trial

MessagingAPIを利用したBotを試すプランです。友だちとメッセージの送受信を行うことができます。
※追加可能友だち数は50人に制限されています。また、Developer Trialからプランの切り替えやプレミアムIDの購入はできません。

フリー

MessagingAPIを利用したBotを開発するプランです。友だちの人数に制限はありませんが、Push messagesを利用してBotから友だちにメッセージを送信することはできません。
※サービス拡張に向けプラン変更が可能です。

3. Botの作成

BotはServerless Frameworkを使って、API gatewayとLambda(ruby)で作成してみます。

serverless frameworkがインストールされている前提で進みますが、インストールからという方は以下を参照してインストールしてください。
https://serverless.com/framework/docs/providers/aws/guide/installation/

3-1. serverlessコマンドでテンプレート作成

$ serverless create --template aws-ruby --path line-bot-test

3-2. serverless.ymlの編集

とりあえず最低限としてこんな感じにしておきます

service: line-bot-test # NOTE: update this with your service name

provider:
  name: aws
  runtime: ruby2.5
  region: ap-northeast-1
  memorySize: 512
  timeout: 900

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: bot/hello
          method: get

3-3. デプロイ

$ serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: WARNING: Function hello has timeout of 900 seconds, however, it's attached to API Gateway so it's automatically limited to 30 seconds.
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service line-bot-test.zip file to S3 (686 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.......................
Serverless: Stack update finished...
Service Information
service: line-bot-test
stage: dev
region: ap-northeast-1
stack: line-bot-test-dev
resources: 11
api keys:
  None
endpoints:
  GET - https://xxxxxx/dev/bot/hello
functions:
  hello: line-bot-test-dev-hello
layers:
  None

とりあえずAPI GatewayとLambda関数がデプロイされました。

4. LINE Developersコンソールからボットを設定する

4-1. チャンネルアクセストークンを発行する

コンソール画面上はChannel Secretのことでコンソール上からこれを発行します。

チャネルアクセストークンとは?

チャネルアクセストークンは長期間有効なアクセストークンで、APIを呼び出すときにAuthorizationヘッダーに設定する必要があります。チャネルアクセストークンはいつでもコンソールで再発行できます。

4-2. Webhook URLを設定する

先ほど作ったAPI gatewayのURLをWebhook URLとして設定します。

Webhook URLとは?

Webhook URLはボットアプリケーションのサーバーのエンドポイントで、Webhookペイロードの送信先です。

コンソール上から接続確認をすると...

スクリーンショット 2019-04-12 22.15.06.png

原因はAPI Gateway側のメソッドがGETになっていることでした...

よくよく考えたらPOSTですよね...

API Gateway側のメソッドをPOSTに直して再デプロイ

$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service line-bot-test.zip file to S3 (686 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...................
Serverless: Stack update finished...
Service Information
service: line-bot-test
stage: dev
region: ap-northeast-1
stack: line-bot-test-dev
resources: 11
api keys:
  None
endpoints:
  POST - https://xxxxxxx/dev/bot/hello
functions:
  hello: line-bot-test-dev-hello
layers:
  None

再度、接続確認

今後はうまくいきました!!

スクリーンショット 2019-04-12 22.23.50.png

これで一旦LINEからBotにメッセージを送ってみます

Image from iOS.png

こんな感じで変なメッセージが返ってきちゃいます...

これはチャネル基本設定 > LINE@基本機能の利用 > 自動応答メッセージを利用しないにする必要があるそうです

スクリーンショット 2019-04-13 0.56.06.png

自動応答メッセージを利用しないにしたので再度試してみます

Image from iOS 2.png

とりあえず自動応答メッセージはなくなりました

5. Lambda関数の作成

Lambda関数がserverless createコマンドで作られたものままなのでちゃんと返信するようにしてきます

とりあえずLINEからどんなリクエストが来るかeventの中身を見てみます

{
    "resource": "/bot/hello",
=================途中省略=================
    "body": "{\"events\":[{\"type\":\"message\",\"replyToken\":\"7e36bf04b9104b26b11b0a2a7df2336c\",\"source\":{\"userId\":\"xxxxccccc\",\"type\":\"user\"},\"timestamp\":1555321188152,\"message\":{\"type\":\"text\",\"id\":\"9697231865658\",\"text\":\"\"}}],\"destination\":\"xxxxxxxx\"}",
    "isBase64Encoded": false
}

肝心なのはbody部分のみです

bodyのみ抽出

{
  "body": "{\"events\":[{\"type\":\"message\",\"replyToken\":\"7e36bf04b9104b26b11b0a2a7df2336c\",\"source\":{\"userId\":\"xxxxxxxx\",\"type\":\"user\"},\"timestamp\":1555321188152,\"message\":{\"type\":\"text\",\"id\":\"9697231865658\",\"text\":\"\"}}],\"destination\":\"xxxxxxx\"}"
}

メッセージイベントの詳細は以下にあります
https://developers.line.biz/ja/reference/messaging-api/#message-event

rubyなのでnet/httpで対応することも可能ですが、LINE公式のline-bot-sdk-rubyが存在することのでこちらを使ってみたいと思います。

def client
  @client ||= Line::Bot::Client.new { |config|
    config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
    config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
  }
end

こんな感じでなんか簡単に作れそうなのでnet/httpでやるより良さそうです

channel_secretchannel_tokenって??

channel_secretとは?

これはLINE Developersコンソールのチャネル基本設定 > 基本情報 > Channel Secret  
から確認出来ます

channel_tokenとは?

これは2種類あるようです。

注:この方法では、30日間有効な短期のチャネルアクセストークンが発行されます。長期のチャネルアクセストークンを発行するには、コンソールにある[再発行]ボタンを使います。長期のアクセストークンは、アカウントの種類やプランによってはご利用いただけません。

https://developers.line.biz/ja/reference/messaging-api/#issue-channel-access-token

長期的なものと短期的なものがあります。

短期的なものはLINE messageing APIにアクセスして発行してもらえます。

長期的なものはLINE Developersコンソールのチャネル基本設定 > メッセージ送受信設定 > アクセストークン(ロングターム)から発行することが出来ます。

channel_secretとchannel_tokenはLambdaの環境変数として設定したいのですが、serverless.ymlに書いちゃうとgithubのリポジトリに残ってしまうのでどうしましょ?
※githubのパブリックリポジトリを使ってる前提です

serverless.yml内で秘匿情報を扱う方法

serverless.yml内で環境変数を参照することが出来ます。
今回はこの方法でいきます。
https://serverless.com/framework/docs/providers/aws/guide/variables/#referencing-environment-variables

こんな感じで使えます。

service: new-service
provider: aws
functions:
  hello:
    name: ${env:FUNC_PREFIX}-hello
    handler: handler.hello

Lambdaのrubyランタイムでgemを使う方法

色々方法はあるかと思いますが、serverless-ruby-packageというserverlessのプラグインを見つけたのでこれを利用したいと思います。

使い方はREADMEを読めば簡単です。

注意点はローカルのrubyのバージョンをLambdaに合わせて2.5.0にしておいてください。
もしくはdockerを使ってgemをインストールするのもありです。

むしろdockerを使ってgemをインストールすることをオススメします。

なぜrubyのバージョンを2.5.0にする必要があるかはvendor/bundle/bundler/setup.rbを見るとわかります

定型文を返すようにする

ほとんどLINE公式のline-bot-sdk-rubyのサンプルのままですが、以下のようなLambda関数で定型文が返ってくるようになりました

load "vendor/bundle/bundler/setup.rb"

require 'json'
require 'line/bot'

def hello(event:, context:)
  message = {
    type: 'text',
    text: 'hello'
  }

  client = Line::Bot::Client.new { |config|
      config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
      config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
  }

  body = event["body"]
  requests = client.parse_events_from(body)
  requests.each do |req|
    case req
    when Line::Bot::Event::Message
      case req.type
      when Line::Bot::Event::MessageType::Text
        response = client.reply_message(req['replyToken'], message)
        p response
      end
    end
  end
end

LINEから試してみるとこんな感じです。

Image from iOS (1).png

とりあえず定型文を返すとこまでいけました。

まとめ

インフラエンジニアだけどコードを書きたいということでしたが、serverless frameworkとの格闘がほとんどでした...

なのでここから役に立つBotとして進化させて、この続編として記事を書きたいと思います。

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

【Rails】 jQueryが動作しない時の対処法 (読み込む順番)

いつも忘れてしまうので備忘録。

ダメなパターン

_head.html.erb
<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

javascript_include_tagjQuery CDN の順番では期待通りになりませんでした。

正のパターン

_head.html.erb
<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

jQuery CDNjavascript_include_tag の順番だと期待値通りの動作!

まとめ

先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)

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

【Rails】 jQueryが動作しない時の対処法 (読み込む順番に気をつけて)

いつも忘れてしまうので備忘録。

ダメなパターン

_head.html.erb
<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

javascript_include_tagjQuery CDN の順番では期待通りになりませんでした。

正のパターン

_head.html.erb
<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

jQuery CDNjavascript_include_tag の順番だと期待値通りの動作!

まとめ

先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)

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

【Rails】 jQuery/jsファイル 読み込む順番

いつも忘れてしまうので備忘録。

ダメなパターン

_head.html.erb
<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

javascript_include_tagjQuery CDN の順番では期待通りになりませんでした。

正のパターン

_head.html.erb
<!-- JQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

<%= stylesheet_link_tag    'style', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'selectordie' %>

jQuery CDNjavascript_include_tag の順番だと期待値通りの動作!

まとめ

先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)

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

Ruby の trunk に(実験的に)パターンマッチ構文が入った!

ブログ記事からの転載です。

Ruby の trunk にパータンマッチ構文が実験的に導入されました。

NOTE: 実験的に導入されたので今後仕様が変わる可能性があるので注意してください。

試してみた

と、言うことで早速試してみました。
case when と構文が似ていますが『抽象的にマッチしつつ、マッチした値を変数にキャプチャすることが出来る』っていうあたりが違います。
簡単な使用例としてはこんな感じになります。

def func *x
  case x
  in [a]
    -a
  in [a, b]
    a + b
  in [a, b, c]
    a * b * c
  end
end

p func(1)
# => -1
p func(2, 3)
# => 5
p func(4, 5, 6)
# => 120

case when と似ていますがパターンマッチでは when ではなくて in というキーワードを使用してマッチする構文を記述します。
上記の場合は Array にマッチしつつ、配列の要素を各変数でキャプチャしています。
無理やり if 文で書くとこんな感じですかね。

def func *x
  if Array === x && x.size == 1
    a, = *x
    -a
  elsif Array === x && x.size == 2
    a, b = *x
    a + b
  elsif Array === x && x.size == 3
    a, b, c = *x
    a * b * c
  end
end

パターンマッチを利用した FizzBuzz

これを利用すると FizzBuzz を次のように書くことが出来ます。

def fizzbuzz a
  case [a % 5 == 0, a % 3 == 0]
  in [true, true]
    "FizzBuzz"
  in [_, true]
    "Fizz"
  in [true, _]
    "Buzz"
  else
    a
  end
end

# self.:fizzbuzz は method(:fizzbuzz) と等価
p (1..15).map &self.:fizzbuzz
# => [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]

上記の場合はキャプチャした変数は使っていませんが『何でも受け取れる要素』として変数のキャプチャを利用しています。
だいぶすっきりと書くことが出来ますね。
余談ですが Ruby 2.7 では .: 演算子で method メソッドを呼び出すことが出来ます。

inHash を渡す

in には Hash を記述することができ、次のような判定を行うことも出来ます。

def validation user
  case user
  in { name: /^[a-z]+$/, age: Integer }
    :ok
  else
    :ng
  end
end

p validation(name: "homu", age: 14)
# => :ok
p validation(name: "42", age: 14)
# => :ng
p validation(name: "mami", age: "14")
# => :ng

これはかなり便利そう。

in User(name, age) 記法

in には in User(name, age) のような記法を記述することも出来ます。
これは例えば次のように利用できます。

class User
  attr_accessor :name, :age

  def initialize name, age
    self.name = name
    self.age = age
  end

  def deconstruct
    [name, age]
  end
end


def show user
  case user
  in User(name, age)
    p "User(name = #{name}, age=#{age})"
  in [name, age]
    p "name=#{name}, age=#{age}"
  end
end

homu = User.new("homu", 14)
show(homu)
# => "User(name = homu, age=14)"
show(["mami", 15])
# => "name=mami, age=15"

in User(name, age) と記述された場合、まず user.kind_of?(User) で判定が行われ、マッチしていれば user.deconstruct メソッドで値のキャプチャが行われます。
#deconstruct メソッドは case when でいう #=== のようなメソッドで、(多分)パターンマッチ時に内部で暗黙的に呼ばれるメソッドになります。
これを定義することで独自クラスに対してもパターンマッチを行うことが出来ます。
例えば Struct#deconstruct を呼び出す事が出来るようになっているので次のように記述することも出来ます。

User = Struct.new(:name, :age)

def show user
  case user
  in User(name, age)
    p "User(name = #{name}, age=#{age})"
  in [name, age]
    p "name=#{name}, age=#{age}"
  end
end

homu = User.new("homu", 14)
show(homu)
# => "User(name = homu, age=14)"
show(["mami", 15])
# => "name=mami, age=15"

これなかなかに便利そう

所感

最初に case in の構文を見たときは戸惑ったんですが、実際に使ってみると結構理にかなった構文になっている事がわかってきました。
変数に値をキャプチャすることが出来るのがかなりパターンマッチっぽくてよい。
あと ArrayHash などが使えるのはもちろんなんですが、 #deconstruct を定義することでいろんなクラスでも応用出来るのでかなり便利そうですね。
今後どうなるのかわからない Ruby 2.7 や Ruby 3.0 で最終的にどうなるのかが楽しみです。
個人的には条件を付加しつつ、変数にキャプチャ出来るようになると嬉しいですねえ。

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

Stripe ConnectのCustomアカウントで、子アカウントへの入金情報などを取得する方法(アカウントヘッダー)

Stripe Connectでの子アカウントに関する情報をAPIで取得したい

ドキュメントが少しわかりづらかったので記事を書きました。
プラットフォーム上で子アカウントの情報(例:決済情報、入金情報)を取得し、マイページなどで表示する方法です。
Stripe APIにはアカウントヘッダーという機能があり、APIを呼び出す上で、どのAccountに紐づいた情報を取得するのかを指定できます。このアカウントヘッダーを使用しない場合は、デフォルトで親アカウントが指定されるようです。

  # 例) 子アカウント(ID: acct_xxxxxxxxxx)に対して行われた最新の入金を3件取得する
    Stripe::Payout.list(
      {limit: 3},
      {stripe_account: "acct_xxxxxxxxxx"}
    )

このように、第一引数にパラメータ、第二引数にacct_idを指定することで、特定のアカウントに基づいた情報を取得できるようになります。

例) ある月の売上の入金額を取得する

私が開発しているCtoCサービスでは、子アカウントの売上の入金スケジュールを月末締め、翌月末払いにしています。そこで欲しい情報は、毎月の入金情報です。
例えば2019年3月に作成された入金情報を取得したい場合は、以下のようにします。

    beginning_of_month = Date.new(2019, 3).to_time.to_i
    end_of_month = Date.new(2019, 3).end_of_month.to_time.to_i

    payout = Stripe::Payout.list(
      {created: {gte: beginning_of_month, lte: end_of_month}}, #gteが以上、lteが以下。日時の範囲指定が独特w
      {stripe_account: "acct_xxxxxxxxxx"}
    )

    # 入金額
    payout.data[0].amount
    # 入金予定日
    Time.at(payout.data[0].arrival_date).strftime("%Y/%m/%d")

間違いなどあればご指摘頂けると嬉しいです。

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

seacrets.yml,.gitignore,環境変数

.bash_profile

環境変数を書くところ
export AWS_SECRET_ACCESS_KEY='ここにCSVファイルに乗っている値をコピー'

seacrets.yml

環境変数、つまり.bash_profileとかに書いたものを読み込む記述を書く
aws_access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %>

.gitignore

github等のリモートリポジトリに送りたくないファイルを書く

APIキー等の秘匿情報をcredentials.ymlで管理する方法

githubにpushしたくない情報はcredentials.yml.encで管理する。
参考:https://qiita.com/NaokiIshimura/items/2a179f2ab910992c4d39

1.cledentials.ymlを編集する

#エディタ設定済みの場合
$rails credentials:edit
#エディタ未設定の場合
$EDITOR="vi" bin/rails credentials:edit

※.bash_profileなどに環境変数:EDITORを指定しておけば、EDITOR="xxx"の指定は不要になります。

2.master_keyを設定する。
.master_keyとは、credentials.yml.encを復号化するためのもの。
.master_keyを知っている人じゃないとcredentials.ymlの内容が読めない仕組み。

デフォルトのmaster keyの値はconfig/master.keyに記載されている。
/config/master.keyが存在しない状態でrails credentials:editコマンドを実行した場合、/config/master.keyが生成される。

ローカル環境ではconfig/.master_keyに存在。

自分のローカル環境以外,例えば本番環境やgitリポジトリからpullしたcredentials.ymlを復号するには?

~/.bash_profileに定義する。

#.bash_profile
RAILS_MASTER_KEY="*********************"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メルカリでハマったところ3

capistranoでデプロイ時にエラー

unicornが起動できないみたい
00:17 unicorn:start
      01 $HOME/.rbenv/bin/rbenv exec bundle exec unicorn -c /var/www/freemarket_47nagoya/current/config/unicorn.rb -E production -D 
      01 master failed to start, check stderr log for details

ログを確認しても何も書かれていなかった
Caused by:
SSHKit::Command::Failed: bundle exit status: 1
bundle stdout: Nothing written
bundle stderr: master failed to start, check stderr log for details

ec2のcurrentディレクトリで実行してもダメ
current]$ unicorn_rails -c config/unicorn.rb -E production

/home/ec2-user/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/unicorn-5.4.1/lib/unicorn/configurator.rb:592:in 
`working_directory': config_file=config/unicorn.rb would not be accessible in working_directory=/var/www/freemarket_47nagoya/releases/current (ArgumentError)

psコマンドって何よ

詳しい情報を見たい時は -efオプションをつけるみたい
参考:https://www.ibm.com/support/knowledgecenter/ja/ssw_aix_71/com.ibm.aix.osdevice/cmd_check_proc_stat.htm

grepコマンド

簡単に言うと検索するコマンド
参考:
https://eng-entrance.com/linux-command-grep#grep

catコマンド

concatnate:連結するの意

環境変数

定義は/etc/environmentに保存することで、サーバ全体に適用される。
環境変数の書き込みはvimコマンドを使用して行う。
環境変数を定義する場所はいくつかある。
左から順に優先される。
本番環境なら、/etc/environment ~/.bash_profile
ローカル環境なら、~/.bash_profile .env.development(gem'dotenv')

gitignore関連の問題

<理由>
一度コミットしたファイルについては、その後.gitignoreに追加したとしてもGitの管理下から外されず、pushされてしまう。

<対処方法>
(例)
・ターミナルで
 > git rm --cached secrets.yml
 を実行しGitで管理されないようにする。

・secrets.ymlには直接キーを記述せず、環境変数を参照するようにする。

各5点

(補足)
環境変数とは、その端末のみからアクセスできる変数です。パスの指定や、今回のようにキーを保存するのに使われます。

Linuxでは.bash_profileに記述をすることで、毎回環境変数を設定しなくても起動時にセットすることができます。

よく使う機能のため、ネット記事等を参考に整理しておくことをお勧めします。

本番環境で出品できなくなった

#current/log/production.rb
ActionView::Template::Error (undefined method `category_id' for #<Item:0x00005623a4362ab8>):

ローカルと本番環境のコードは同じに見える。
itemsテーブルにcategory_idがあることは確認。
悩むこと半日ぐらい。
最終的にインスタンスの再起動で治りました。
デプロイでハマったらインスタンス再起動しましょう。

本番環境でrails c した時にエラー

Can't connect to local MySQL server through socket '/tmp/mysql.sock' (13)

本番環境でコンソールを使うときは

rails c production

らしい。

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

getsメソッド

getsメソッドとは

今までターミナルは文字を出力できるようにするだけで、
こちらからターミナルに入力することはできませんでした。
そこで、ターミナルへ入力できるようにする方法がgetsメソッドです。

特徴

1. 返り値としてターミナルで入力された文字列を返す

getsメソッドはターミナルからユーザーに入力を行わせ、入力された値の文字列オブジェクトを返り値として渡します。

2. ターミナルからの入力が終わるまでプログラムの処理を一回止める

getsメソッドを使うとユーザーがターミナルで入力をするまでそこで一回
プログラムを中断します。
よってgetsメソッドより下のソースコードは入力が終わるまでは
実行されないということです。
※普通rubyのプログラムは上から下へと流れていく。

まとめ

getsメソッドを使うと、ユーザーからの入力を変数に代入することができます。
ただ、このままでは入力時に文言などが何も表示されないため、
getsメソッドの前に表示させたい文言をputsで入力することで、
何を入力すべきかわかる。

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

Railsチュートリアルで抑えておくべきポイント

minitestの書き方

なぜコントローラーのテストを実装するのか
- ルーティング通りにアクセスし、期待するページを表示出来ているかどうか
- 表示されたページの文言が期待するものであるかどうか

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get static_pages_home_url
    assert_response :success
   #特定のHTMLタグが存在するかどうかをテストします 
   #(この種のアサーションメソッドはその名から「セレクタ」と呼ばれることもある。
    assert_select "title", "Home | #{@base_title}"
  end
end

setupメソッドは、テストが実行される直前で呼ばれる。
→リファクタリングで使用。

# この書き方は、application.html.rbが無い場合。railsを使うならこの方法は一般的では無い。
<% provide(:title, "Home") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Sample App</h1>

↓railsで実装するなら↓

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>
  </head>
app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
  sample application.
</p>

provideの:titleが空の場合、無駄な「|」が入ってします。これを防ぐために、カスタムヘルパーを定義する。

app/helpers/application_helper.rb
module ApplicationHelper

  # ページごとの完全なタイトルを返します。
# メソッド定義とオプション引数。
#引数にデフォルト値を含めている。 (この例のデフォルト値は空の文字列です)。このように指定すると、page_title変数に引数を渡すことも渡さないこともできます。引数を渡さない場合は、指定のデフォルト値(この場合は空の文字列)を渡すことができる。
  def full_title(page_title = '') 
    base_title = "Ruby on Rails Tutorial Sample App"
    if page_title.empty?
      base_title
    else
      page_title + " | " + base_title
    end
  end
end

カスタムヘルパーを定義し、以下のように修正をする。

application.html.erb
<title><%= full_title(yield(:title)) %></title>

Unixのプロセス

プロセスを確認できる。

$ ps aux

プロセスの種類を指定してフィルタするには、psの結果をUnixの「パイプ」|でつないで、パターンマッチャーであるgrepに渡す。
※Springは「rails server」「rails console」「rake」コマンドが速くなる、Railsの新しいプリローダー

$ ps aux | grep spring

$ ps aux | grep server

プロセスid、略してpidを使い。不要なプロセスを排除するには、killコマンドでpidを指定し、Unixのkillコードを発行。

# 9はシグナル番号。
$ kill -9 [PID]

開発中に動作がおかしくなったりプロセスがフリーズしていると思えたら、すぐにps auxで状態を確認し、kill -15 やpkill -15 -f <プロセス名>で整理してみましょう。

$ spring stop

# spring stopが効かなかったら、下記で一気にspringプロセスをkillする。
$ pkill -9 -f spring

盲点

シングルクォートは、入力した文字をエスケープせずに「そのまま」保持するときに便利。

>> '\n'       # 'バックスラッシュ n' をそのまま扱う
=> "\\n"

# ”\n”になると改行を意味してしまう。"\\n"をシングルクォートを使えば、実装が楽。

オブジェクトとは (いついかなる場合にも) メッセージに応答するものです。
Ex,文字列のようなオブジェクトは、例えばlengthというメッセージに応答する

# if文の違う書き方
puts "x is not empty" if !x.empty?

#「!!」(「バンバン (bang bang)」と読みます) という演算子を使うと、そのオブジェクトを2回否定することになるので、どんなオブジェクトも強制的に論理値に変換できる
>> !!nil
=> false
>> !!0
=> true

Rubyのメソッドには「暗黙の戻り値がある」ことにご注意。これは、メソッド内で最後に評価された式の値が自動的に返されることを意味します (訳注: メソッドで戻り値を明示的に指定しなかった場合の動作です)
なので、以下のような場合は、returnが必要。

def string_message(str = '')
  return "It's an empty string!" if str.empty?
  "The string is nonempty."
end

splitメソッド(文字列を処理して配列を返す)

>>  "foo bar     baz".split     # 文字列を3つの要素を持つ配列に分割する
=> ["foo", "bar", "baz"]

>> "name".split('')
=> ["n", "a", "m", "e"]
>> "name".split
=> ["name"]

>> "fooxbarxbaz".split('x')
=> ["foo", "bar", "baz"]

joinメソッド(splitの逆の処理。配列を処置して文字列を出力)

>> a
=> [42, 8, 17, 6, 7, "foo", "bar"]
>> a.join                       # 単純に連結する
=> "4281767foobar"
>> a.join(', ')                 # カンマ+スペースを使って連結する
=> "42, 8, 17, 6, 7, foo, bar"
>> a = %w[foo bar baz quux]         # %wを使って文字列の配列に変換
=> ["foo", "bar", "baz", "quux"]
>> a = (0..9).to_a
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>> ('a'..'e').to_a
=> ["a", "b", "c", "d", "e"]

# 範囲(レンジ)と配列は蜜に関係している。
 (1..5).each { |i| puts 2 * i }

>> (1..5).map { |i| i**2 }          # 「**」記法は冪乗 (べき乗) 
=> [1, 4, 9, 16, 25]

# mapとjoinは繋げられる
def yeller(s)
  s.map(&:upcase).join
end
=> :yeller

yeller(['o','l','d'])
=> "OLD"

# 以上ことを把握できれば、以下が理解できる。
('a'..'z').to_a.shuffle[0..7].join

>> p :name             # 'puts :name.inspect' と同じ
:name
>> params = {}        # 'params' というハッシュを定義する ('parameters' の略)。
=> {}
>> params[:user] = { name: "Michael Hartl", email: "mhartl@example.com" }
=> {:name=>"Michael Hartl", :email=>"mhartl@example.com"}
>> params
=> {:user=>{:name=>"Michael Hartl", :email=>"mhartl@example.com"}}
>>  params[:user][:email]
=> "mhartl@example.com"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアルで押さえておくべきポイント

minitestの書き方

なぜコントローラーのテストを実装するのか
- ルーティング通りにアクセスし、期待するページを表示出来ているかどうか
- 表示されたページの文言が期待するものであるかどうか

require 'test_helper'

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get static_pages_home_url
    assert_response :success
   #特定のHTMLタグが存在するかどうかをテストします 
   #(この種のアサーションメソッドはその名から「セレクタ」と呼ばれることもある。
    assert_select "title", "Home | #{@base_title}"
  end
end

setupメソッドは、テストが実行される直前で呼ばれる。
→リファクタリングで使用。

# この書き方は、application.html.rbが無い場合。railsを使うならこの方法は一般的では無い。
<% provide(:title, "Home") %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Sample App</h1>

↓railsで実装するなら↓

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all',
                               'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application',
                               'data-turbolinks-track': 'reload' %>
  </head>
app/views/static_pages/home.html.erb
<% provide(:title, "Home") %>
<h1>Sample App</h1>
<p>
  This is the home page for the
  <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
  sample application.
</p>

provideの:titleが空の場合、無駄な「|」が入ってします。これを防ぐために、カスタムヘルパーを定義する。

app/helpers/application_helper.rb
module ApplicationHelper

  # ページごとの完全なタイトルを返します。
# メソッド定義とオプション引数。
#引数にデフォルト値を含めている。 (この例のデフォルト値は空の文字列です)。このように指定すると、page_title変数に引数を渡すことも渡さないこともできます。引数を渡さない場合は、指定のデフォルト値(この場合は空の文字列)を渡すことができる。
  def full_title(page_title = '') 
    base_title = "Ruby on Rails Tutorial Sample App"
    if page_title.empty?
      base_title
    else
      page_title + " | " + base_title
    end
  end
end

カスタムヘルパーを定義し、以下のように修正をする。

application.html.erb
<title><%= full_title(yield(:title)) %></title>

Unixのプロセス

プロセスを確認できる。

$ ps aux

プロセスの種類を指定してフィルタするには、psの結果をUnixの「パイプ」|でつないで、パターンマッチャーであるgrepに渡す。
※Springは「rails server」「rails console」「rake」コマンドが速くなる、Railsの新しいプリローダー

$ ps aux | grep spring

$ ps aux | grep server

プロセスid、略してpidを使い。不要なプロセスを排除するには、killコマンドでpidを指定し、Unixのkillコードを発行。

# 9はシグナル番号。
$ kill -9 [PID]

開発中に動作がおかしくなったりプロセスがフリーズしていると思えたら、すぐにps auxで状態を確認し、kill -15 やpkill -15 -f <プロセス名>で整理してみましょう。

$ spring stop

# spring stopが効かなかったら、下記で一気にspringプロセスをkillする。
$ pkill -9 -f spring

文法における盲点

シングルクォートは、入力した文字をエスケープせずに「そのまま」保持するときに便利。

>> '\n'       # 'バックスラッシュ n' をそのまま扱う
=> "\\n"

# ”\n”になると改行を意味してしまう。"\\n"をシングルクォートを使えば、実装が楽。

オブジェクトとは (いついかなる場合にも) メッセージに応答するものです。
Ex,文字列のようなオブジェクトは、例えばlengthというメッセージに応答する

# if文の違う書き方
puts "x is not empty" if !x.empty?

#「!!」(「バンバン (bang bang)」と読みます) という演算子を使うと、そのオブジェクトを2回否定することになるので、どんなオブジェクトも強制的に論理値に変換できる
>> !!nil
=> false
>> !!0
=> true

Rubyのメソッドには「暗黙の戻り値がある」ことにご注意。これは、メソッド内で最後に評価された式の値が自動的に返されることを意味します (訳注: メソッドで戻り値を明示的に指定しなかった場合の動作です)
なので、以下のような場合は、returnが必要。

def string_message(str = '')
  return "It's an empty string!" if str.empty?
  "The string is nonempty."
end

splitメソッド(文字列を処理して配列を返す)

>>  "foo bar     baz".split     # 文字列を3つの要素を持つ配列に分割する
=> ["foo", "bar", "baz"]

>> "name".split('')
=> ["n", "a", "m", "e"]
>> "name".split
=> ["name"]

>> "fooxbarxbaz".split('x')
=> ["foo", "bar", "baz"]

joinメソッド(splitの逆の処理。配列を処置して文字列を出力)

>> a
=> [42, 8, 17, 6, 7, "foo", "bar"]
>> a.join                       # 単純に連結する
=> "4281767foobar"
>> a.join(', ')                 # カンマ+スペースを使って連結する
=> "42, 8, 17, 6, 7, foo, bar"
>> a = %w[foo bar baz quux]         # %wを使って文字列の配列に変換
=> ["foo", "bar", "baz", "quux"]
>> a = (0..9).to_a
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>> ('a'..'e').to_a
=> ["a", "b", "c", "d", "e"]

# 範囲(レンジ)と配列は蜜に関係している。
 (1..5).each { |i| puts 2 * i }

>> (1..5).map { |i| i**2 }          # 「**」記法は冪乗 (べき乗) 
=> [1, 4, 9, 16, 25]

# mapとjoinは繋げられる
def yeller(s)
  s.map(&:upcase).join
end
=> :yeller

yeller(['o','l','d'])
=> "OLD"

# 以上ことを把握できれば、以下が理解できる。
('a'..'z').to_a.shuffle[0..7].join

>> p :name             # 'puts :name.inspect' と同じ
:name
>> params = {}        # 'params' というハッシュを定義する ('parameters' の略)。
=> {}
>> params[:user] = { name: "Michael Hartl", email: "mhartl@example.com" }
=> {:name=>"Michael Hartl", :email=>"mhartl@example.com"}
>> params
=> {:user=>{:name=>"Michael Hartl", :email=>"mhartl@example.com"}}
>>  params[:user][:email]
=> "mhartl@example.com"
>> class String             # WordクラスはStringクラスを継承する
>>   # 文字列が回文であればtrueを返す
>>   def palindrome?
>>     self == self.reverse        # selfは文字列自身を表します
>>   end
>> end
=> :palindrome?

selfはオブジェクト(インスタンス)をさす。
self == reverseと、selfの省略も可能。

"aaa"はStringクラスのインスタンス

IE9への対応

念のため、以下のタグを

タグに入れた方が良い
<!--[if lt IE 9]>
  <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js">
 </script>
<![endif]-->

 画像の格納場所

assetのコンパイル対象にしたいならassets配下。
ブラウザのキャッシュコントロールのためのバージョン管理化に置かれなくていいならpublic配下に置く。基本的にはシステム側で作る画像はassets以下、ユーザー側のアップロードする画像はpublic配下に置いておく。

ただ、表示速度を重視したい背景画像などは、public配下に置くと良い。
↓先頭に/(スラッシュ)をつけると、public配下になる。

<%= image_tag '/images/hoge.png' %>

yarnを使わず、gemでbootstrapを導入する方法

https://github.com/twbs/bootstrap-rubygem

# できるだけ、カスタムCSSは、一つのファイルで編集する
$ touch app/assets/stylesheets/custom.scss
app/assets/stylesheets/custom.scss
# 以下をimport
@import "bootstrap";
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

devise NoMethodError (undefined method `utc'

こんなエラーが出てしまった場合

I, [2019-04-17T02:04:34.636864 #7612]  INFO -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] Started GET "/users/sign_in" for 133.218.55.173 at 2019-04-17 02:04:34 +0000
I, [2019-04-17T02:04:34.637545 #7612]  INFO -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] Processing by Users::SessionsController#new as HTML
D, [2019-04-17T02:04:34.639131 #7612] DEBUG -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d]    (0.2ms)  SET NAMES utf8mb4 COLLATE utf8mb4_bin,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
D, [2019-04-17T02:04:34.639915 #7612] DEBUG -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d]   User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
I, [2019-04-17T02:04:34.641703 #7612]  INFO -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] Completed 500 Internal Server Error in 4ms (ActiveRecord: 1.0ms)
F, [2019-04-17T02:04:34.642646 #7612] FATAL -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d]
F, [2019-04-17T02:04:34.642770 #7612] FATAL -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] NoMethodError (undefined method `utc' for Sat, 06 Apr 2019:Date):
F, [2019-04-17T02:04:34.642858 #7612] FATAL -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d]
F, [2019-04-17T02:04:34.642973 #7612] FATAL -- : [18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] devise (4.5.0) lib/devise/models/rememberable.rb:118:in `remember_me?'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] devise (4.5.0) lib/devise/models/rememberable.rb:143:in `serialize_from_cookie'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] devise (4.5.0) lib/devise/strategies/rememberable.rb:22:in `authenticate!'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] warden (1.2.8) lib/warden/strategies/base.rb:54:in `_run!'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] warden (1.2.8) lib/warden/proxy.rb:369:in `block in _run_strategies_for'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] warden (1.2.8) lib/warden/proxy.rb:365:in `each'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] warden (1.2.8) lib/warden/proxy.rb:365:in `_run_strategies_for'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] warden (1.2.8) lib/warden/proxy.rb:335:in `_perform_authentication'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] warden (1.2.8) lib/warden/proxy.rb:110:in `authenticate'
[18ca5a07-0693-4dd5-8be1-ac3c7c5ea47d] warden (1.2.8) lib/warden/proxy.rb:120:in `authenticate?'

ローカルのクッキーを削除

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

bundle installすると"can't find gem bundler…"というエラーが発生

bundle installでエラーが出る

エラー内容

bundle installを実行したところ、
find_spec_for_exe': can't find gem bundler (>= 0.a) (Gem::GemNotFoundException)
というエラーが発生してbundleinstallできない。

原因

rubyとbundlerのバージョンに注意!

rubygemとbundlerのバージョンの組み合わせによって発生するエラーらしい。

解決法

  • bundlerのバージョンを下げる

最新ではなく少し前のバージョンを指定してinstallしてあげる。

$ gem install bundler -v '1.17.3'

そもそもの確認

  • rubyとbundlerの場所を確認する。

rubyとbundlerが片方ローカルを参照していて、もう片方はグローバルを参照しているなど、そもそもローカルにgemが入っていなかったケース。

$ which ruby
/Users/hoge/.rbenv/shims/ruby
$ which bundler
/Users/hoge/.rbenv/shims/bundler

参考

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

【初心者向け】1番初めにやる、Ruby on Rails環境構築(Mac)

はじめに

Railsチュートリアル以降ずっとcloud9で開発してきましたが、cloud9上のエラーに時間を使ってしまったり、読み込みが遅かったり、業務に支障が出たりするので、流石にそろそろ1からしっかりと開発環境を作ってみようと思います。

環境構築

①Command Line Toolsインストール

コマンドラインツールとは、プログラミングの環境を構築したり実際にプログラムを実行したりする際に、コマンドを入力して操作ができるアプリケーション、CUI(キャラクターユーザーインターフェース)のことです。

(↔︎マウスなどで操作する通常アプリケーション:GUI(グラフィカルユーザーインターフェース)

Xcodeのバージョンが6.1以降の場合、Command Line Toolsは自動的にインストールされるので、Xcodeをダウンロードしていきます。

①Appleの「developerアカウント」にログイン
②『Download Tools』へ
③「Xcode」ダウンロード

ちなみにXcodeは、
・テキストエディタ
・インターフェースの作成(Interface Builder)
・デバッグ
・ビルド
・テスト
・シミュレーター(iOS Simulator)
・ソースの管理
などの必須機能を備えたものです。

②Homebrewインストール

Homebrewとは、Mac OS Xオペレーティングシステム上でソフトウェアの導入を単純化するパッケージ管理システムのひとつです。

①あるかどうか確認(Xcodeが入ればHomebrewも入っているはず)

$ brew -v

②念のためアップデート

$ brew update

③rbenvインストール

rbenvとは、~/.rbenv/以下で、インストールした様々なRubyバージョンを管理し、状況に応じて必要になるRubyのバージョンを簡単に切り替えてくれるコマンドラインツールです。

①あるかどうか確認

$ rbenv -v

②念のためアップデート

$ brew upgrade rbenv

③無ければインストール

$ brew install rbenv ruby-build

④Rubyの最新バージョンをインストール

①インストール可能なRubyのバージョンを確認

$ rbenv install --list

②現時点で最新の「2.7.0-dev」を入れる

$ rbenv install 2.7.0-dev
$ rbenv global 2.7.0-dev
$ rbenv rehash

③バージョンが反映されているか確認

$ ruby -v

⑤Bundlerインストール

Bundlerとは、gem同士の互換性を保ちながらパッケージの種類やバージョンを管理してくれるgemです。
複数人、または複数環境で開発を行う際、各環境で使用するものに合わることができます。

Ruby2.6.0よりBundlerは標準添付されてるので、上記の通りやっていれば既にインストールされているはずです。

①念のため確認

$ bundle -v

②念のためアップデート

$ gem update bundler

③無ければインストール(Ruby2.5.5以前を使用していた場合、等)

$ gem install bundler

⑥MySQLインストール

(SQLiteを使用する場合この設定は不要です)

MySQLとは、世界で最も利用されているデータベース管理システムです。

LAMP環境(「Linux」+「Apache」+「MySQL」+「PHP(またはRuby)」)でサーバーを構築する企業が多く、よく利用されています。

①MySQLをインストール

$ brew install mysql

②起動

$ mysql.server start

Starting MySQL
. SUCCESS! 

⑦Railsインストール

最後です。
GemfileにRailsを加え、インストールしていきます。
これができて、晴れて画面表示されます。

①作業するディレクトリを作成

$ mkdir ~/workspace
$ cd ~/workspace

②Rubyのバージョンを指定

$ rbenv local 2.7.0-dev

作業ディレクトリに.ruby-versionファイルが作成される

③Gemfileを作成する

$ bundle init

④Gemfile内の「# gem "rails"」のコメント解除

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails"

⑤Railsをインストール

$ bundle install --path=vendor/bundle
$ bundle exec rails -v
Rails 5.2.3

⑥Railsアプリ作成

$ bundle exec rails new test_app

⑦Webサーバー起動

$ rails server

http://localhost:3000/ へアクセス
下記の画面が表示されれば成功です。
スクリーンショット 2019-04-16 16.17.18.png

⑨※サーバーが立ち上がらない

railsはしっかり入っているのに、rails serverを実行すると

$ rails server
Rails is not currently installed on this system. To get the latest version, simply type:

    $ sudo gem install rails

You can then rerun your "rails" command.

と出て実行してくれませんでしたが、

下記を実行した後に無事動くようになりました。

$ gem install railties
$ rbenv rehash

上の画像が表示されました!!

参考

最速!MacでRuby on Rails環境構築
Ruby初学者のRuby On Rails 環境構築【Mac】
railsコマンドが使えないときにやったこと

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

RubyでAdWords APIを実行するときはadwords_api.ymlに注意

RubyでさくっとAdWords APIをたたいてみようかと思ったら、思わぬところではまったので、共有します。

大まかなRubyでAdWords APIを利用するまでの手順はこんな感じです。

  1. gemのgoogle-adwords-apiをインストールする
  2. 設定ファイルのadwords_api.ymlを用意する
  3. サービス等の処理が書いてあるrbファイルを実行する

この中で2.のadwords_api.ymlを用意する際に注意が必要です。

公式のリファレンスの中でadwords_api.ymlのサンプルはこのようになっています。(必要な部分のみ抜粋)

[...]
:oauth2_client_id: xxxxxxxxxx.apps.googleusercontent.com
:oauth2_client_secret: zZxxxxxTxxxxxxxxxxx
:refresh_token: 1/dyOIp7ki-xxxxxxxxxxxxxxxxxxxxxxxx
:developer_token: 123axxxxxxxxxxxxxxxxxx
[...]

しかし、この通りに設定してもうまく動きません。
実際に動く形はこう。

:authentication:
  :oauth2_client_id: xxxxxxxxxx.apps.googleusercontent.com
  :oauth2_client_secret: zZxxxxxTxxxxxxxxxxx
  :oauth2_token:
    :refresh_token: 1/dyOIp7ki-xxxxxxxxxxxxxxxxxxxxxxxx
  :developer_token: 123axxxxxxxxxxxxxxxxxx

refresh_tokenoauth2_tokenの下の階層が正しいです。
ちなみに、expires_atoauth2_tokenの下になります。

おまけ

adwords_api.yml$HOMEをデフォルトで探しに行きますが、API実行時に動的にパスを指定することもできます。

同じ階層にある場合
require "adwords_api"

config_filename = File.join(Dir.getwd, "adwords_api.yml")
adwords = AdwordsApi::Api.new(config_filename)

managed_customer_srv = adwords.service(:ManagedCustomerService, :v201809)

参考

詳しい手順や情報はこちらの記事を参照。

最初の API 呼び出しを実行する | AdWords API | Google Developers
Home · googleads/google-api-ads-ruby Wiki
AdWords APIをRubyで一通り試してみる

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

rbenvの使い方まとめ

インストールされているバージョンの確認

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

【学習アウトプット3】has_secure_passwordとvalidates :password, presence: trueの違い

背景

has_secure_passwordを設定したUserモデル

user.rb
class User < ApplicationRecord
  has_secure_password

が、以下のテスト

user_controller_test.rb
test "password should be exist when create" do
  @user.password = " "
  assert_not @user.valid?
end

で引っかかってしまう問題に悩んでいました。
(has_secure_passwordは空白にvalidationがかかると思っていたため https://qiita.com/Shantti-Y/items/19ea23b81f3421063fc5)

validates :password, presence: trueをつけるとテストをパスします。
じゃあ、has_secure_passwordのデフォルトvalidationとは一体...:rolling_eyes:

結論

Rails 4.2から以下のように変更されていたとのことです。

has_secure_password がデフォルトで空白のパスワードを許容するようになりました (例: 空白スペースのみのパスワード)。 https://railsguides.jp/4_2_release_notes.html

空白を許可したくない場合は別途validates :password, presence: true等の追加が必要みたいです。

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

Rubyは最後に評価された式が戻り値になるというが、どのif文にも入らなかったらどうなるのか

経緯

  • Rubyを学び始めた頃のコードを直しているのですが、Javaエンジニアだったためかメソッド内に無駄なreturn文が多いです。
  • 直す最中に表題の件について気になったので、Rails cを使って試し、またRailsのビューから呼ばれた時はどうなるのかについても試してみました。

修正前

  • ひどい書き方ですね。。。
game.rb
  def get_msg
    msg = ""
    if self.gameset_flag
      if get_sum_top == get_sum_bottom #同クラス内の別のインスタンスメソッドを呼んでいます
        msg = "(引き分け)"
      elsif !self.top6
        msg = "(5回コールド)"
      elsif !self.top7
        msg = "(6回コールド)"
      elsif !self.top8
        msg = "(7回コールド)"
      elsif !self.top9
        msg = "(8回コールド)"
      end
    end
    return msg
  end

修正後

  • このメソッドをビューから呼んだ時に、どのif文にも入らなかった場合は、ちゃんと空文字で画面表示できるのか気になっていました。
game.rb
  def get_msg
    if self.gameset_flag
      if get_sum_top == get_sum_bottom #同クラス内の別のインスタンスメソッドを呼んでいます
        "(引き分け)"
      elsif !self.top6
        "(5回コールド)"
      elsif !self.top7
        "(6回コールド)"
      elsif !self.top8
        "(7回コールド)"
      elsif !self.top9
        "(8回コールド)"
      end
    end
  end

Rails c で動かしてみる

  • コード修正は今回の主旨ではないため、修正後のコードのみで試します。
irb(main):001:0> game = Game.new
(出力省略)
irb(main):002:0> game.get_sum_top
=> 0
irb(main):003:0> game.get_sum_bottom
=> 0

irb(main):004:0> game.gameset_flag = false
=> false
irb(main):005:0> game.get_msg
=> nil

irb(main):006:0> game.gameset_flag = true 
=> true
irb(main):007:0> game.get_msg
=> "(引き分け)"
  • どのif文にも入らない場合は、最初のif文の条件式が最後に評価されたことになります。
  • つまり self.gameset_flagnil ということで nil が返って来ているのがわかります。

画面表示はどうなる

  • 実際の画面キャプチャは貼れないのですが、結論としては問題ありませんでした(空文字がreturnされていた頃と表示に変化なし)。
  • つまり、修正前のコードのように、どのif文にも入らなかった時のための変数の初期化は不要ということになります(もちろん設計上空文字を返却する必要がある場合は、メソッドの最終行に空文字を書いて、空文字が最後に評価されるようにすると良いと思います)。

参考

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