「いつもの」が結構ありそうなので書いておく。
app.rb ペラ 1 でツラくなったときの対策はだいたい sonots パイセンの ちっちゃくはじめておっきく育てる sinatra アプリの作り方 に書いてあって、これは今でも有効なので読んでおくと良いです。
ディレクトリ構成
REPO ├── app.rb ├── bin/ ├── config/ │ ├── database.yml │ ├── initializers/ │ └── locales/ ├── config.ru ├── Gemfile ├── Gemfile.lock ├── helpers/ ├── models/ ├── public/ └── views/
- sinatra らしさをなるべく残してある
- 例えば
config/boot.rb
を用意するかは非常に悩んだのだけれど、起点は app.rb であって欲しい
- 例えば
models/
,helpers/
は分けた- app.rb に直接書いていると 5 model ぐらいしか無くても数百行になってさすがに見通しが悪いので
config/initializers/
を置いた- 置き場所に迷ったものを放り込みやすくて便利
もう一声大きくなったらもっと Rails のディレクトリ構造に近づける *1 けど、一旦こんな感じで。
セキュリティ対策
erubi を使う
XSS 対策のために escape: true
を使いたい。
https://github.com/jeremyevans/erubi/blob/1.9.0/lib/erubi.rb#L57
erubi gem を入れた上で
class App < Sinatra::Base set :erb, escape: true ...
しておくと <%= ... %>
がデフォルトで escape される。
String ごとに html_safe
フラグを付けられるわけではないので ActiveSupport::SafeBuffer ほど便利ではないが、無いより 100 億倍良い。
これは完全に余談なんだけど Padrino は SafeBuffer が導入されていて羨ましい。
- https://github.com/padrino/padrino-framework/blob/0.15.0/padrino-helpers/lib/padrino/safe_buffer.rb
- https://github.com/padrino/padrino-framework/blob/0.15.0/padrino-helpers/lib/padrino/rendering/erubi_template.rb
ところで Erubi には「escape
が優先される」と書いてあるので escape: true
って使ったんだけど、検索しても 1 件も引っかからないのな。
escape_html: true
で検索するとめちゃくちゃ尊い PR が出てきた。(先月じゃーん
Rack::Protection を使う
デフォルトで使われているんだけど、remote_token であって authenticity_token ではないので置き換える。
set :protection, use: %i[authenticity_token], except: %i[remote_token]
remote_token は referrer が同じだったら通している。 https://github.com/sinatra/sinatra/blob/v2.0.8.1/rack-protection/lib/rack/protection/remote_token.rb#L17-L19
authenticity_token は (GET, HEAD, OPTIONS, TRACE 以外は) 常にチェックする。 https://github.com/sinatra/sinatra/blob/v2.0.8.1/rack-protection/lib/rack/protection/authenticity_token.rb#L98-L106
これで例え XSS があったとしても CSRF されなくなって安心。
ところでこの記事書こうと思ってついでに自社の過去事例を漁っていたら 14 年前の面白記述を見つけた。サードパーティ製ツールを大事にしている精神が感じられて最高でした。
SessionStore を Cookie から変更
クライアント側に構造体を保存しているとセッションの無効化が困難、みたいなアレです。
ユーザごとのログインセッションを任意に破棄できるようにしようと思うとログイン処理のたびに 1 レコード増えることになって、「素朴な Web アプリケーションが許される時代は終わったなぁ」って気がしますね。
よく使う extension, middleware
ほとんど Sinatra::Contrib だな。
Sinatra::ConfigFile
YAML を置いておくとアプリケーション内から設定にアクセスできるマン。
大枠で言うと Rails::Application.config_for
と同じ使い心地です。
Sinatra::ContentFor
# layout.erb <title><% if content_for?(:title) %><%= yield_content(:title) %> | <% end %>APP_NAME</title>
# users/show.erb <% content_for :title, @user.name %>
みたいなヤツ。 title の例だけで無いとツラいのが分かるはず。
Sinatra::JSON
json @obj
すると JSON dump して content-type 付けて返してくれるヤツ。
Sinatra::Reloader
development で有効にすると開発が楽で幸せ。
configure :development do register Sinatra::Reloader also_reload "models/**/*" also_reload "helpers/**/*" end
Rack::Flash
rack-flash3 gem でやってる。
redirect 時に notice を渡す書き方もしたいので、Padrino::Flash からパクっておきます。
REPL
bin/console にこれを置いておくだけでだいぶ楽。
#!/usr/bin/env ruby require "bundler/setup" require "irb" require File.expand_path("../app", __dir__) def reload! Sinatra::Reloader.perform(App) end IRB.start(__FILE__)
reload!
は ActiveSupport::Dependencies と違って丁寧に remove_const
とかをやっているわけではなくただ require し直しているだけだけど、model を読み込み直したいぐらいの用途なら足る。
initialize
ディレクトリ構成のところで言った config/initializers
を一番最初に読み込ませている。
Dir[File.expand_path("config/initializers/*", __dir__)].sort.each {|f| require f }
ActiveRecord
以下を食わせている。前半は establish_connection
用。後半は timestamp 周り。
path = File.expand_path("config/database.yml", __dir__) specs = YAML.safe_load(ERB.new(IO.read(path)).result, aliases: true) ActiveRecord::Base.configurations = specs.stringify_keys ActiveRecord::Base.establish_connection(env.to_sym) Time.zone_default = Time.find_zone!("Asia/Tokyo") ActiveRecord::Base.time_zone_aware_attributes = true ActiveRecord::Base.default_timezone = :utc
前半は sinatra-activerecord gem で良いんだけど、メンテが滞ってるのを見て から使わなくなってしまった。薄いのでシュッと剥がせたし。最近は次に名乗り出てくれたメンテナが元気なので大丈夫かもしれない。
timestamp は Rails だと Railtie が良い感じにやってくれているが、生で ActiveRecord を使うときは自分で指定する必要がある。 https://github.com/rails/rails/blob/v6.0.3/activerecord/lib/active_record/railtie.rb#L69-L74
あとは models/
以下に配置して、 ApplicationRecord
も欲しいので
class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end
require_relative "application_record" class User < ApplicationRecord ... end
のような感じにしてある。
ActiveRecord::RecordNotFound
ActiveRecord::RecordNotFound
を 500 じゃなく 404 で返したいので以下を書く。
error ActiveRecord::RecordNotFound do |e| not_found # 404 HTML を返したかったらこう # send_file "public/404.html", status: 404 end
I18n
Time を出力するときに I18n.l
を使うと楽なので YAML を読み込ませておく。
# config/initializers/i18n.rb I18n.load_path << Dir[File.expand_path("config/locales/*.yml")]
RSS/Atom
builder gem を使え!
ログイン
current_user
と authenticate_user!
さえあれば事足りることが多いので、 omniauth だけを使っている。
# application_helper.rb def current_user return nil unless session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end
# app.rb get "/auth/github/callback" do ... redirect env["omniauth.origin"] || "/" end private def authenticate_user! return if current_user if request.safe? redirect "/auth/github?origin=#{CGI.escape(request.fullpath)}" else redirect "/auth/github" end end
テスト
request spec
rspec-request_describer gem にお世話になっています。send_request
を書いておけば rack-test
で使える。
routing がカオスになってきたらリクエストを投げた後に last_request.env["sinatra.route"]
を確認することで、routing spec の真似事ができる。
RSpec.describe "/", type: :request do let(:app) { App } let(:send_request) { send(http_method, path, request_body, env).status } describe "GET /" do it { is_expected.to eq 200 expect(last_request.env["sinatra.route"]).to eq "GET /" } } }
fixture
https://github.com/rspec/rspec-rails/blob/v4.0.1/lib/rspec/rails/file_fixture_support.rb をパクってきて spec/support/ 以下に置いてます。
module FileFixtureSupport extend ActiveSupport::Concern include ActiveSupport::Testing::FileFixtures included do self.file_fixture_path = File.join(File.expand_path("..", __dir__), "fixtures", "files") end end RSpec.configure do |config| config.include FileFixtureSupport end
database_rewinder
頑張れば use_transactional_tests
使えるかもしれないけど、読み解くのサボっているので愛用しています。
まとめ
これぐらいの準備で割と Rails と似た書き味で書けるし、だいたい Sinatra::Contrib にあるから安心って感じですね。Padrino に標準装備されているものが多いのですごく悩ましい……。padrino-helpers に嫌な思い出があるだけで Padrino は悪くないので使えば良いのかもしれない。
デプロイしたのが先週なので、運用が始まったらもっと色々考えることが増えていきそうだと思う。例えばログ。(今は標準出力 && Rack::CommonLogger のままで一旦いいやって思って手を付けていない)
*1:GitHub を数時間泳いだところ、https://github.com/cesarfigueroa/acme がめちゃくちゃ良いので参考にしたい