- 投稿日:2019-04-17T23:24:14+09:00
ハッシュ関数とは
ハッシュ関数とは
プログラミング初心者なりに『ハッシュ関数とは』何かをまとめてみた。
ハッシュ関数とは
一言でいうと『入力されたデータに対して適当な値を返してくれる関数。』
そしてその返ってきた値をハッシュと呼ぶ。
ちなみに関数にいれるものは『引数』呼ぶ。
ハッシュは引数として関数にいれた値をぐちゃぐちゃにしてハッシュとして返してくれる特性があるため、第三者にばれてはいけないパスワードなどの設定になどに使われる。
ちょっと汚い話になるけれど、
ラーメン(引数)を食べたら
それが体(関数)に入って
うんち(ハッシュ)としてでてくる。うんちを見た人は一体何を食べたのか分からない。
つまりハッシュは、入力されたデータに対して適当な値を返してくれる関数。だと思えばOKだと思う。
- 投稿日:2019-04-17T22:51:11+09:00
mrubyでの30日開発日記(3日目)
今日やったこと
- 数学を学ぶ
- hiroを読んでみた
やってわかったこと
- 構造体でのポインタのこととポインタのことが少しわかった。 記事を書いたので読んでみてください!https://qiita.com/eizi2342/items/d568286582c5364c5387
- 数学は四元数がゲームで使われているのを知ってびっくりした。
今日のコラム
昨日、ゲームを朝にやったどうなるか、ということを言ったが、結果は成功だった。
まじで目が覚める。15分ゲームやってみたけどすぐに目が冴えた。本当におすすめ!Ciao!!
- 投稿日:2019-04-17T22:44:04+09:00
声優名で話しかけると出演するゲームを教えてくれるLINE BOTを作ったったー
友達登録してくれると喜びます!!
どんなBOTなの?
声優名で話しかけると出演するゲームを5件まで教えてくれます!!
話しかけ方として以下の3つがあります。
- 声優名
- 先月の声優名
- 来月の声優名
例えば「風音」さんの出演しているゲームを調べたい時はBOTに対して
- 風音
- 先月の風音
- 来月の風音
と話しかけます。
実行タイミングが4月12日(金)なので
風音
の場合は2019年4月に発売するゲームを教えてくれ、同様に先月の風音
の場合は2019年3月で来月の風音
の場合は2019年5月の出演するゲームを教えてくれます。
出演するゲームがない場合は出演する予定はありませんと教えてくれます。声優名以外にも「リスト」と話しかけるとゲームの発売リストページを教えてくれます。
話しかけ方は声優名と同じで以下の3つがあります。
- リスト
- 先月のリスト
- 来月のリスト
仕組みは?
Ruby(スクレイピングツール)とGASを使用して作成しています。
ざっくりとした図は下記のとおりです。スクレイピングツール
げっちゅ屋の発売日リストとゲームの紹介ページをスクレイピングしその結果をコマンドラインにて表示、絞り込みできるツールになります。
https://github.com/dodonki1223/eroge_release_cmdこのスクレイピングツールでゲームの発売リスト情報を取得しCSVに出力します。
出力したCSVの内容を元に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構文を書く時に参考にしたこと
- オブジェクト指向スクリプト言語 Ruby リファレンスマニュアル (Ruby 2.6.0)
- rubocop-hq/rubocop: A Ruby static code analyzer and formatter, based on the community Ruby style guide.
- Cookpadのコーディング規約
公式サイトこそ正義だと思っているので、必ず公式サイトのリファレンスを読んでプログラムを書いていきました。
公式サイトの内容がよくわからない時はブログや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 endVCRという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 endGoogleスプレッドシートの操作
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 endgoogle-drive-rubyのコードのRSpecはMockを多用して書きました。
量が多いのでリンクだけ貼っておきます。興味がある人は読んで見て下さい。LINE BOTの開発について
LINE BOT
LINEの社員さんが書かれているQiitaの記事を參考にLINEの設定を行いました。
Node.jsのプログラム開発以外はこちらを參考にしました。LINE BOTとGASとの連携
下記記事を參考にしました。
- Google Apps ScriptでLINE BOTつくったら30分で動かせた件 - Qiita
- Google Spreadsheet を簡易 Webサーバーとして動かして、手軽にWebHookを受け取る方法 - Qiita
LINE BOTとGASの連携は本当に簡単です。1時間もあれば簡単に連携することができました。
LINE Developersの設定で
Webhook送信
を利用する
にし忘れることがあります。
利用する
にしてないとLINEから文字を入力してもうんともすんとも反応しないので必ず利用する
になっているか確認して下さい。GASについて
今回のGASの役割はLINEからの入力を受け取り、受け取った内容でスプレッドシート内を検索し合致した内容をLINEに返すことをしています。
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スプレッドシートには年月の単位でシートができています。
シート名で取得できるようにしています。シートの取得方法
// 対象の月を取得する // 先月、来月などの文字から対象月を取得する // 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通知でめんどくさいのが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); }設定方法としては、スクリプトエディタからトリガーを設定するだけです。
トリガー設定画面
設定は関数単位で指定できるのでreleaseListSendMessage
を設定します。この設定で1週間ごと、毎週金曜日にSlackに通知されるようになります。
まとめ
Googleスプレッドシートへの書き込みがまだ手動なので今後はここを自動化したいと思っています。
開発していて一番時間がかかったのはRubyのRSpecです。1ヶ月以上戦っていた気がします。LINE BOTとGASに関しては触ったことがなかったのですが10時間ぐらいでできました。
10時間ぐらいなので休みの日をかけてLINE BOTを余裕で作成できるレベルです。皆さんも気軽にLINE BOTを作成してみるのものいいかもしれません!
- 投稿日:2019-04-17T22:32:52+09:00
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!
をコンソールに入力すれば良いらしい。
- 投稿日:2019-04-17T22:14:44+09:00
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を行うと無事、デプロイが成功するようになりました。
- 投稿日:2019-04-17T22:14:44+09:00
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を行うと無事、デプロイが成功するようになりました。
- 投稿日:2019-04-17T22:05:08+09:00
【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
と入力する。これでherokuなどにデプロイしなくても、スマホから画面確認などが出来ます。
また、httpなのでwiresharkなどでパケットキャプチャーすれば中身が見れます。
- 投稿日:2019-04-17T22:05:08+09:00
【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
と入力する。これでherokuなどにデプロイしなくても、スマホから画面確認などが出来ます。
また、httpなのでwiresharkなどでパケットキャプチャーすれば中身が見れます。
- 投稿日:2019-04-17T21:31:30+09:00
Railsの部分テンプレ(partial)をラッピングして、collection変数を渡す
TL;DR
_item.html.slimh3 Item Name: #{item.name} p Item Index: #{item_counter}_wrapped_item.html.slimh2 Item Wrapper .item-area = render partial: 'item',\ locals: { item: wrapped_item, item_counter: wrapped_item_counter }index.html.slimh1 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.slimh3 Item Name: #{item.name} p Item Index: #{item_counter}_wrapped_item.html.slimh2 Item Wrapper .item-area = render partial: 'item'index.html.slimh1 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_item
をitem
として渡しました。もちろん、Indexも必要な時は、#{variable_name}_count
も伝播しなければいけません。_wrapped_item.html.slimh2 Item Wrapper .item-area = render partial: 'item',\ locals: { item: wrapped_item, item_counter: wrapped_item_counter }参考
- 投稿日:2019-04-17T20:53:52+09:00
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?
andafter?
methods to date and time classes で、
refactoring の PR が Move implementation ofbefore?
andafter?
toDateAndTime::Calculations
です。参考情報
- 投稿日:2019-04-17T20:32:02+09:00
インフラエンジニアだけどLINE Botを作りたい
はじめに
お仕事的にはインフラエンジニアですが、昨今の技術の進歩からインフラエンジニアいらないんじゃない的なことを感じたり感じなかったりなので、インフラエンジニアでもある程度コードかけないとまずいとは思ってるけど、じゃいざ何作るの?って段階になるとなかなかアイデアで出ない人は多いのではないのでしょうか。
そんな人は簡単なLINE Botを作ってみるとこから初めて見たらいかがでしょうか?という記事です。
環境
何かしらでBotサーバーを用意しなければなりません。
選択肢としては、GASやHerokuなどが思いつくと思いますが、普段私がAWSしかいじってないのでAPI Gateway + Lambda(Ruby)をserverless frameworkでデプロイします。普段AWSをいじってる人はこの構成でやることを是非オススメします
手順
ここからはLINE Botが定型文を返すまでの手順となります
1. LINE@アカウントの取得
まずは以下のリンクからLINE@アカウント(一般アカウント)を取得する必要があります。
一般アカウントとは?
「一般アカウント」は審査なしで作成できるアカウントです。
一般アカウントの作成後に認証済みアカウントを申し込むことも可能です。
ご利用可能なプランはお住まいの国によって異なります。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-test3-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: get3-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ペイロードの送信先です。
コンソール上から接続確認をすると...
原因は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再度、接続確認
今後はうまくいきました!!
これで一旦LINEからBotにメッセージを送ってみます
こんな感じで変なメッセージが返ってきちゃいます...
これはチャネル基本設定 > LINE@基本機能の利用 > 自動応答メッセージを利用しないにする必要があるそうです
自動応答メッセージを利用しないにしたので再度試してみます
とりあえず自動応答メッセージはなくなりました
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-eventrubyなので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_secretとchannel_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.helloLambdaの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 endLINEから試してみるとこんな感じです。
とりあえず定型文を返すとこまでいけました。
まとめ
インフラエンジニアだけどコードを書きたいということでしたが、serverless frameworkとの格闘がほとんどでした...
なのでここから役に立つBotとして進化させて、この続編として記事を書きたいと思います。
- 投稿日:2019-04-17T20:23:12+09:00
【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_tag
→jQuery 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 CDN
→javascript_include_tag
の順番だと期待値通りの動作!まとめ
先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)
- 投稿日:2019-04-17T20:23:12+09:00
【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_tag
→jQuery 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 CDN
→javascript_include_tag
の順番だと期待値通りの動作!まとめ
先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)
- 投稿日:2019-04-17T20:23:12+09:00
【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_tag
→jQuery 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 CDN
→javascript_include_tag
の順番だと期待値通りの動作!まとめ
先にjQueryロードしないとスクリプト読めないって話。(Railsに限った話じゃない・・・)
- 投稿日:2019-04-17T18:59:42+09:00
Ruby の trunk に(実験的に)パターンマッチ構文が入った!
ブログ記事からの転載です。
Ruby の trunk にパータンマッチ構文が実験的に導入されました。
- Introduce pattern matching [EXPERIMENTAL] · ruby/ruby@9738f96 · GitHub
- Feature #14912: Introduce pattern matching syntax - Ruby trunk - Ruby Issue Tracking System
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
メソッドを呼び出すことが出来ます。
in
にHash
を渡す
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
の構文を見たときは戸惑ったんですが、実際に使ってみると結構理にかなった構文になっている事がわかってきました。
変数に値をキャプチャすることが出来るのがかなりパターンマッチっぽくてよい。
あとArray
やHash
などが使えるのはもちろんなんですが、#deconstruct
を定義することでいろんなクラスでも応用出来るのでかなり便利そうですね。
今後どうなるのかわからない Ruby 2.7 や Ruby 3.0 で最終的にどうなるのかが楽しみです。
個人的には条件を付加しつつ、変数にキャプチャ出来るようになると嬉しいですねえ。
- 投稿日:2019-04-17T17:41:26+09:00
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")間違いなどあればご指摘頂けると嬉しいです。
- 投稿日:2019-04-17T17:23:58+09:00
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/2a179f2ab910992c4d391.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="*********************"
- 投稿日:2019-04-17T17:23:21+09:00
メルカリでハマったところ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.htmgrepコマンド
簡単に言うと検索するコマンド
参考:
https://eng-entrance.com/linux-command-grep#grepcatコマンド
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らしい。
- 投稿日:2019-04-17T17:14:58+09:00
getsメソッド
getsメソッドとは
今までターミナルは文字を出力できるようにするだけで、
こちらからターミナルに入力することはできませんでした。
そこで、ターミナルへ入力できるようにする方法がgetsメソッドです。特徴
1. 返り値としてターミナルで入力された文字列を返す
getsメソッドはターミナルからユーザーに入力を行わせ、入力された値の文字列オブジェクトを返り値として渡します。
2. ターミナルからの入力が終わるまでプログラムの処理を一回止める
getsメソッドを使うとユーザーがターミナルで入力をするまでそこで一回
プログラムを中断します。
よってgetsメソッドより下のソースコードは入力が終わるまでは
実行されないということです。
※普通rubyのプログラムは上から下へと流れていく。まとめ
getsメソッドを使うと、ユーザーからの入力を変数に代入することができます。
ただ、このままでは入力時に文言などが何も表示されないため、
getsメソッドの前に表示させたい文言をputsで入力することで、
何を入力すべきかわかる。
- 投稿日:2019-04-17T15:02:49+09:00
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 endsetupメソッドは、テストが実行される直前で呼ばれる。
→リファクタリングで使用。# この書き方は、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.rbmodule 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 => trueRubyのメソッドには「暗黙の戻り値がある」ことにご注意。これは、メソッド内で最後に評価された式の値が自動的に返されることを意味します (訳注: メソッドで戻り値を明示的に指定しなかった場合の動作です)
なので、以下のような場合は、returnが必要。def string_message(str = '') return "It's an empty string!" if str.empty? "The string is nonempty." endsplitメソッド(文字列を処理して配列を返す)
>> "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"
- 投稿日:2019-04-17T15:02:49+09:00
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 endsetupメソッドは、テストが実行される直前で呼ばれる。
→リファクタリングで使用。# この書き方は、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.rbmodule 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 => trueRubyのメソッドには「暗黙の戻り値がある」ことにご注意。これは、メソッド内で最後に評価された式の値が自動的に返されることを意味します (訳注: メソッドで戻り値を明示的に指定しなかった場合の動作です)
なので、以下のような場合は、returnが必要。def string_message(str = '') return "It's an empty string!" if str.empty? "The string is nonempty." endsplitメソッド(文字列を処理して配列を返す)
>> "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.scssapp/assets/stylesheets/custom.scss# 以下をimport @import "bootstrap";
- 投稿日:2019-04-17T11:13:22+09:00
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?'ローカルのクッキーを削除
- 投稿日:2019-04-17T10:50:15+09:00
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参考
- 投稿日:2019-04-17T09:19:56+09:00
【初心者向け】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/ へアクセス
下記の画面が表示されれば成功です。
⑨※サーバーが立ち上がらない
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コマンドが使えないときにやったこと
- 投稿日:2019-04-17T09:04:31+09:00
RubyでAdWords APIを実行するときはadwords_api.ymlに注意
RubyでさくっとAdWords APIをたたいてみようかと思ったら、思わぬところではまったので、共有します。
大まかなRubyでAdWords APIを利用するまでの手順はこんな感じです。
- gemの
google-adwords-api
をインストールする- 設定ファイルの
adwords_api.yml
を用意する- サービス等の処理が書いてある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_token
はoauth2_token
の下の階層が正しいです。
ちなみに、expires_at
もoauth2_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で一通り試してみる
- 投稿日:2019-04-17T01:44:41+09:00
【学習アウトプット3】has_secure_passwordとvalidates :password, presence: trueの違い
背景
has_secure_passwordを設定したUserモデル
user.rbclass User < ApplicationRecord has_secure_passwordが、以下のテスト
user_controller_test.rbtest "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とは一体...結論
Rails 4.2から以下のように変更されていたとのことです。
has_secure_password がデフォルトで空白のパスワードを許容するようになりました (例: 空白スペースのみのパスワード)。 https://railsguides.jp/4_2_release_notes.html
空白を許可したくない場合は別途
validates :password, presence: true
等の追加が必要みたいです。
- 投稿日:2019-04-17T00:33:01+09:00
Rubyは最後に評価された式が戻り値になるというが、どのif文にも入らなかったらどうなるのか
経緯
- Rubyを学び始めた頃のコードを直しているのですが、Javaエンジニアだったためかメソッド内に無駄なreturn文が多いです。
- 直す最中に表題の件について気になったので、
Rails c
を使って試し、またRailsのビューから呼ばれた時はどうなるのかについても試してみました。修正前
- ひどい書き方ですね。。。
game.rbdef 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.rbdef 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_flag
はnil
ということでnil
が返って来ているのがわかります。画面表示はどうなる
- 実際の画面キャプチャは貼れないのですが、結論としては問題ありませんでした(空文字がreturnされていた頃と表示に変化なし)。
- つまり、修正前のコードのように、どのif文にも入らなかった時のための変数の初期化は不要ということになります(もちろん設計上空文字を返却する必要がある場合は、メソッドの最終行に空文字を書いて、空文字が最後に評価されるようにすると良いと思います)。
参考