Contents
⚓ 1. 環境
⚓ 2. 要件
このアプリケーションは、以下の二つの要件を満たすUIを提供する。
機能が少ないので、Ruby on Rails を選択する主な選択理由である ActiveRecord
やデータベースを持たない非常にシンプルな構成にしている。
さらに、2022年01月16日現在の最新版である Rails 7.0.1
がフロントエンドパッケージの Webpacker
と疎結合になった。
これにより、このアプリケーションはさらにシンプルになった。
⚓ 3. ソースコード
⚓ 3-1. 訓練データ生成
⚓ 3-1-1. ルーティング
Rails.application.routes.draw do # TrainingDataController resource :training_data, only: [:new] post '/training_data/download', to: 'training_data#download' get '/training_data/download', to: redirect('/training_data/new')
Prefix | Verb | URI Pattern | Controller#Action |
---|---|---|---|
new_training_data | GET | /training_data/new(.:format) | training_data#new |
training_data_download | POST | /training_data/download(.:format) | training_data#download |
GET | /training_data/download(.:format) | redirect(301, /training_data/new) |
⚓ 3-1-2. コントローラー
⚓ 3-1-2-1. TrainingDataController
TrainingDataController#new
はフォームビューに共有するTrainingDataForm
クラスインスタンスを生成するのみ。TrainingDataController#download
はtraining_params
経由でCSVフォーマットの訓練データを受け取る。パラメータが有効な場合、#training_data_#{実行時の年月日時分秒}.json
のファイル名でJSONフォーマットの訓練データをダウンロードできる。パラメータが不正の場合、"Training data can't be blank" という警告が表示され、TrainingDataControllernew
をレンダリングし、アクションを抜ける。
class TrainingDataController < ApplicationController include Format def new @training_data_form = TrainingDataForm.new end def download @training_data_form = TrainingDataForm.new(training_params) if @training_data_form.valid? send_data(to_json(training_params[:training_data]), filename: filename) else render :new end end private def training_params params.permit(:training_data) end def filename "training_data_#{DateTime.current.strftime('%F%T').gsub(/[:\-]/, '')}.json" end end
⚓ 3-1-2-2. Format
Botpress回答精度検証 in Ruby -訓練データ生成- > 3-2-2. Format と全く同じ。
⚓ 3-1-3. フォームオブジェクト
ApplicationForm
は validates
メソッドを利用するため、ActiveModel::Model
をインクルードする。
class ApplicationForm include ActiveModel::Model end
TrainingDataForm#attr_accessor
はtraining_data
属性の読み込み・書き込みの両方のアクセスを提供する。TrainingDataForm#validates
はparams[:training_data]
の存在を検証する。もし是である場合、TrainingDataForm#valid?
はtrue
を返し、さもなくばfalse
を返す。
class TrainingDataForm < ApplicationForm attr_accessor :training_data validates :training_data, presence: true end
⚓ 3-1-4. ビュー
<h1>Create JSON Training Data</h1> <%= form_with url: training_data_download_path, method: :post do |f| %> <%= render 'shared/errors', object: @training_data_form %> <div class="container"> <p><%= f.label :training_data %></p> <p><%= f.file_field :training_data, accept: '.csv' %></p> <p><%= f.submit 'Export JSON', class: 'btn btn-primary' %></p> </div> <% end %>
⚓ 3-1-5. テスト
- CSVフォーマットの訓練データが適切に選択されている場合、JSONフォーマットの訓練データのダウンロードに成功する。
- CSVフォーマットの訓練データが適切に選択されていない場合、"Training data can't be blank" というバリデーションエラーが表示される。
require 'rails_helper' RSpec.describe "TrainingData", type: :system do before do visit new_training_data_path end describe 'training data conversion from CSV to JSON' do context 'A CSV file is chosen' do it 'succeeds in converting CSV file to JSON file' do expect(page).to have_current_path new_training_data_path attach_file 'Training data', "#{Rails.root}/csv/training_data.csv" click_on 'Export JSON' expect(downloaded_file).to match(/training_data.*json/) end end context 'No CSV file is chosen' do it 'fails to convert CSV file to JSON file with a validation message shown' do expect(page).to have_current_path new_training_data_path click_on 'Export JSON' expect(page).to have_selector '.alert-danger', text: "Training data can't be blank" end end end end
⚓ 3-2. 回答精度検証
⚓ 3-2-1. ルーティング
Rails.application.routes.draw do # ScoreChartController resource :score_chart, only: [:new, :create], controller: :score_chart get '/score_chart/draw', to: 'score_chart#draw' get '/score_chart/download', to: 'score_chart#download', default: { format: :csv } get '/score_chart', to: redirect('/score_chart/new') end
Prefix | Verb | URI Pattern | Controller#Action |
---|---|---|---|
new_score_chart | GET | /score_chart/new(.:format) | score_chart#new |
score_chart | POST | /score_chart(.:format) | score_chart#create |
score_chart_draw | GET | /score_chart/draw(.:format) | score_chart#draw |
score_chart_download | GET | /score_chart/download(.:format) | score_chart#download {:default=>{:format=>:csv}} |
GET | /score_chart(.:format) | redirect(301, /score_chart/new) |
⚓ 3-2-2. コントローラー
⚓ 3-2-2-1. ScoreChartController
ScoreChartController#new
はフォームビューに共有するScoreChartForm
クラスインスタンスを生成するのみ。このアクションが呼ばれるとき、before_action
で指定されたChartManager#clear_tmp_charts
がtmp/downloads
内に保存された全ての一時保存されたCSVフォーマットのマトリックス図を削除するコールバック関数として発火する。ScoreChartController#create
は以下の処理を行う。scheme
、host
、bot_id
、user_id
、access_token
、test_data
を含むtest_params
を受け取る。もしパラメータが有効な場合、次の処理に進む。さもなくば、ScoreChartController#new
をレンダリングし、アクションを抜ける。test_params
を引数に取りScoreChartQuery
クラスインスタンスを生成し、ScoreChartQuery#res_bodies
を実行する。成功の場合、Rubyのハッシュにパースしたレスポンスボディの配列を返す。さもなくば、"Host is invalid" という警告を表示し、ScoreChartController#new
をレンダリングし、アクションを抜ける。test_params[:test_data]
とres_bodies
を引数に取りChartDrawer
クラスインスタンスを生成し、ChartDrawer#csv
を実行する。成功の場合、CSVフォーマットのマトリックス図を返す。さもなくば、"BotID、UserID or AccessToken is invalid" という警告を表示し、ScoreChartController#new
をレンダリングし、アクションを抜ける。- CSVフォーマットのマトリックス図のデータを
ScoreChartController#draw
アクションと共有するため一時保存する。アクション間の変数の共有の目的でクラス変数@@csv_chart
を使うのは、典型的なアンチパターンなので避けるべきである。 - このアクションでHTMLフォーマットのマトリックス図を描画すると、ページリロードに
404
エラーになってしますう。これは、このアクションがPOST
メソッドで行われており、リロード時はGET
メソッドでルーティングを参照するからである。そういう訳で。GET
メソッドでアクセスする別のパスを切り、そちらにリダイレクトした上でHTMLフォーマットのマトリックス図を描画する。
ScoreChartController#draw
は、一時保存されたCSVフォーマットのマトリックス図が存在する場合に、文字通りHTMLフォーマットのマトリックス図を描画する。さもなくば、ScoreChartController#new
をレンダリングし、アクションを抜ける。ScoreChartController#download
では、一時保存されたCSVフォーマットのマトリックス図が存在する場合に、文字通りCSVフォーマットのマトリックス図を#accuracy_score_chart_#{実行時の年月日時分秒}.csv
のファイル名でダウンロードできる。さもなくば、ScoreChartController#new
をレンダリングし、アクションを抜ける。
class ScoreChartController < ApplicationController include ChartManager before_action :clear_tmp_charts, only: [:new] def new @score_chart_form = ScoreChartForm.new end def create @score_chart_form = ScoreChartForm.new(test_params) if @score_chart_form.valid? score_chart_query = ScoreChartQuery.new(test_params) begin res_bodies = score_chart_query.res_bodies rescue SocketError flash[:alert] = 'Host is invalid' render :new and return end chart_drawer = ChartDrawer.new(test_params[:test_data], res_bodies) begin csv_chart = chart_drawer.csv rescue NoMethodError flash[:alert] = 'BotID, UserID or AccessToken is invalid' render :new and return end save_chart(filename, csv_chart) redirect_to score_chart_draw_url else render :new end end def draw redirect_to new_score_chart_url and return unless tmp_chart @chart = matrix_chart end def download redirect_to new_score_chart_url and return unless tmp_chart send_file(tmp_chart, filename: filename) end private def test_params params.permit(:scheme, :host, :bot_id, :user_id, :access_token, :test_data) def filename "accuracy_score_chart_#{DateTime.current.strftime('%F%T').gsub(/[:\-]/, '')}.csv" end end
⚓ 3-2-2-2. ChartManager
- CSVフォーマットのマトリックス図を一時保存するパスである
tmp/downloads
をPATH
定数に代入する。 ChartManager#save_chart
はtmp/downloads
が存在しない場合はそれを作り、filename
とcsv_chart
を引数に取りCSVフォーマットのマトリックス図を一時保存する。ChartManager#tmp_charts
はtmp/downloads
内のaccuracy_score_chart_#{YYMMDDhhmmss}.csv
に合致する全てのファイル名を返す。ChartManager#tmp_chart
はtmp/downloads
内の 最後の要素を返す。ChartManager#matrix_chart
はtmp/downloads
内にCSVフォーマットのマトリックス図があればそれを返す。ChartManager#clear_tmp_charts
はtmp/downloads
内の全てのファイルを削除する。これはScoreChartController#new
アクションで呼ばれる。
require 'csv' module ChartManager PATH = Rails.root.join('tmp', 'downloads') def save_chart(filename, csv_chart) Dir.mkdir(PATH) unless Dir.exist?(PATH) File.open(PATH.join(filename), 'w') { |f| f.puts(csv_chart) } end def tmp_charts Dir[PATH.join('accuracy_score_chart*.csv')] end def tmp_chart tmp_charts.last end def matrix_chart CSV.open(tmp_chart) { |f| f.read } if tmp_chart end def clear_tmp_charts FileUtils.rm_f(tmp_charts) if tmp_charts.any? end end
⚓ 3-2-3. フォームオブジェクト
TrainingDataForm#attr_accessor
はscheme
、host
、bot_id
、user_id
、access_token
、test_data
属性の読み込み・書き込みの両方アクセスを提供する。TrainingDataForm#validates
params[:scheme]
、params[:host]
、params[:bot_id]
、params[:user_id]
、params[:access_token]
、params[:test_data]
の存在を検証する。もし是である場合、TrainingDataForm#valid?
はtrue
を返し、さもなくばfalse
を返す。
class ScoreChartForm < ApplicationForm attr_accessor :scheme, :host, :bot_id, :user_id, :access_token, :test_data validates :scheme, presence: true validates :host, presence: true validates :bot_id, presence: true validates :user_id, presence: true validates :access_token, presence: true validates :test_data, presence: true end
⚓ 3-2-4. クエリ
Botpress回答精度検証 in Ruby -回答精度検証- > 3-2-2. AccuracyCheckQuery とほぼ同じだが、以下の差分がある。
ScoreChartQuery#initialize
はtest_params
を引数にとり、各パラメータをキーで参照している。params[:access_token]
(Bearerトークン)がユーザー入力である。
... class ScoreChartQuery INVALID_PATTERNS = /[\\\'\|\`\^\"\<\>\)\(\}\{\]\[\;\/\?\:\@\&\=\+\$\,\%\# ]/ def initialize(test_params) @scheme = test_params[:scheme] @host = test_params[:host].gsub(INVALID_PATTERNS, '') @bot_id = test_params[:bot_id].gsub(INVALID_PATTERNS, '') @user_id = test_params[:user_id].gsub(INVALID_PATTERNS, '') @access_token = test_params[:access_token].chomp @test_data = CSV.read(test_params[:test_data], headers: true) end ...
⚓ 3-2-5. ライブラリ
Botpress回答精度検証 in Ruby -回答精度検証- > 3-2-3. ChartDrawer と全く同じ。
⚓ 3-2-6. ビュー
⚓ 3-2-6-1. Create Score Chart
<h1>Create Score Chart</h1> <%= form_with url: score_chart_url do |f| %> <%= render 'shared/errors', object: @score_chart_form %> <div class="form-item"> <p><strong><%= f.label :scheme %></strong></p> <p><%= f.select :scheme, ['https', 'http'], { selected: 'https' } %></p> </div> <div class="form-item"> <p><strong><%= f.label :host %></strong></p> <p><%= f.text_field :host, size: 50, placeholder: 'sample.com' %></p> </div> <div class="form-item"> <p><strong><%= f.label :bot_id %></strong></p> <p><%= f.text_field :bot_id, size: 30, placeholder: 'sample-bot' %></p> </div> <div class="form-item"> <p><strong><%= f.label :user_id %></strong></p> <p><%= f.text_field :user_id, size: 30, placeholder: 'sample-user' %></p> </div> <div class="form-item"> <p><strong><%= f.label :access_token %></strong></p> <p><%= f.text_area :access_token, size: '100x5', placeholder: 'Bearer ...' %></p> </div> <div class="form-item"> <p><strong><%= f.label :test_data %></strong></p> <p><%= f.file_field :test_data, accept: '.csv' %></p> </div> <div class="form-item"> <p><%= f.submit 'Create Score Chart', class: 'btn btn-primary' %></p> </div> <% end %>
⚓ 3-2-6-2. Accuracy Score Chart
<h1>Accuracy Score Chart</h1> <p><%= link_to 'Download Score Chart', score_chart_download_url, class: 'btn btn-primary' %></p> <div class="scroll-table"> <table> <% @chart&.each do |chart| %> <tr> <% chart&.each do |row| %> <th class=<%= accuracy(row) %>><%= row %></th> <% end %> </tr> <% end %> </table> </div> <div class="annotaion"> <h2>Annotation</h2> <p class="excellent">Greater than or equal to 70.0%</p> <p class="good">Greater than or equal to 50.0% and less than 70.0%</p> <p class="bad">Greater than or equal to 30.0% and less than 50.0%</p> <p class="useless">Greater than or equal to 0.1% and less than 30.0%</p> </div>
⚓ 3-2-7. Helper
ScoreChartHelper#accuracy
は回答精度を評価し、レンジに応じてセルの色分けを行う。
- 70.0%以上: 青
- 50.0%以上70%未満: 緑
- 30.0%以上50%未満: 黄色
- 30.0%未満: 赤
- 0%もしくは回答精度と無関係のセル: デフォルトの背景色(白)
⚓ 3-2-8. テスト
- 全てのパラメータが有効な場合、, HTMLフォーマットのマトリックス図を描画し、その行数がCSVフォーマットのテストデータの行数と一致する。
- 一時保存されたCSVフォーマットのマトリックス図が存在する場合、ダウンロードが成功しファイル名が
accuracy_score_chart_#{実行時の年月日時分秒}.csv
に一致する。 new_score_chart_path
にアクセスすると、tmp/downloads
の全てのファイルが削除される。- 一時保存されたCSVフォーマットのマトリックス図が存在しない場合、
score_chart_draw_path
にアクセスするとnew_score_chart_path
にリダイレクトする。 - 一時保存されたCSVフォーマットのマトリックス図が存在しない場合、
score_chart_download_path
にアクセスするとnew_score_chart_path
にリダイレクトする。 - パラメータが一つも与えられない場合、以下のバリデーションエラーが発生する。
- ホスト名が不正な場合、"Host is invalid" という警告が表示される。
- ボットID、ユーザーID、アクセストークンのいずれかが不正な場合、"BotID, UserID or AccessToken is invalid" という警告が表示される。
require 'rails_helper' require 'csv' require 'fileutils' RSpec.describe "ScoreChart"、type: :system do before do visit new_score_chart_path end describe 'HTML accuracy score chart' do context 'All required parameters are fulfilled' do let(:number_of_test_data) { CSV.read("#{Rails.root}/csv/test_data.csv").size } before do select 'https'、from: 'Scheme' fill_in 'Host'、with: ENV['HOST'] fill_in 'Bot'、with: ENV['BOT_ID'] fill_in 'User'、with: ENV['USER_ID'] fill_in 'Access token'、with: ENV['ACCESS_TOKEN'] attach_file 'Test data'、"#{Rails.root}/csv/test_data.csv" click_on 'Create Score Chart' end it 'succeeds in rendering HTML accuracy score chart' do expect(page).to have_current_path score_chart_draw_path expect(all('tbody tr').size).to eq(number_of_test_data) end end end describe 'CSV accuracy score chart' do context 'Any temporary CSV accuracy score chart exists in ./tmp/downloads' do before do FileUtils.cp "#{Rails.root}/csv/accuracy_score_chart_20211130220255.csv"、"#{Rails.root}/tmp/downloads/" end it 'succeeds in downloading CSV accuracy score chart' do visit score_chart_download_path expect(downloaded_file).to match(/accuracy_score_chart.*csv/) end it 'clears all tmp_charts when visiting new_score_chart_path' do visit new_score_chart_path expect(downloads).to eq([]) end end context 'No temporary CSV accuracy score chart exists in ./tmp/downloads' do it 'redirects to new_score_chart_path' do visit new_score_chart_path expect(page).to have_current_path new_score_chart_path end end end describe 'validation and error handling' do context 'No required parameters are fulfilled' do before do click_on 'Create Score Chart' end it 'fails to render HTML accuracy score chart with validation errors shown' do expect(page).to have_current_path score_chart_path expect(page).to have_selector '.alert-danger'、text: "Host can't be blank" expect(page).to have_selector '.alert-danger'、text: "Bot can't be blank" expect(page).to have_selector '.alert-danger'、text: "User can't be blank" expect(page).to have_selector '.alert-danger'、text: "Access token can't be blank" expect(page).to have_selector '.alert-danger'、text: "Test data can't be blank" end end context 'Invalid Host' do before do select 'https'、from: 'Scheme' fill_in 'Host'、with: 'foo' fill_in 'Bot'、with: ENV['BOT_ID'] fill_in 'User'、with: ENV['USER_ID'] fill_in 'Access token'、with: ENV['ACCESS_TOKEN'] attach_file 'Test data'、"#{Rails.root}/csv/test_data.csv" click_on 'Create Score Chart' end it 'fails in rendering HTML matrix chart and an error message is shown' do expect(page).to have_current_path score_chart_path expect(page).to have_selector '.alert-danger'、text: 'Host is invalid' end end context 'Invalid BotID、UserID and AccessToken' do before do select 'https'、from: 'Scheme' fill_in 'Host'、with: ENV['HOST'] fill_in 'Bot'、with: 'foo' fill_in 'User'、with: 'bar' fill_in 'Access token'、with: 'piyo' attach_file 'Test data'、"#{Rails.root}/csv/test_data.csv" click_on 'Create Score Chart' end it 'fails in rendering HTML matrix chart and an error message is shown' do expect(page).to have_current_path score_chart_path expect(page).to have_selector '.alert-danger'、text: 'BotID、UserID or AccessToken is invalid' end end end end