LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

Sinatraで社内サービスを作るためのいろいろ

自分用めも。

予め書いておく。
Sinatraで画面をもつ&ログインが必要な&Herokuデプロイ可能なアプリケーションを書くのは結構めんどくさいので、たとえ単機能であってもRailsを使ったほうがいい。間違いない。

前提

フォーム入力したらそれに沿って外部サービスのAPIを叩きに行って、結果を表示するだけのアプリ。一応ActiveRecordは使用する。

ただし社内サービスなので、フォームバリデーションとかは適当でいい。あとは

  • Docker, docker-composeで開発環境はサッと作れるのがよい。
  • Herokuにデプロイしたい。
  • GitHub認証で、なおかつ知らないユーザは弾きたい。

開発用のDockerファイルを作る。

Railsに比べると依存は少ない。

docker-compose.yml
version: "3"
services:
  web:
    build: .
    volumes:
      - .:/sinatra-inhouse-app
      - ruby-bundle:/usr/local/bundle
    working_dir: /sinatra-inhouse-app

volumes:
  ruby-bundle:
    driver: local
FROM ruby:2.7-alpine

Gemfileを作る

$ docker-compose build
$ docker-compose run --rm web sh
/sinatra-inhouse-app # bundle init

SinatraでHello Worldするまで

このへんまでは楽勝。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

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

gem "sinatra", require: false

↑sinatraに require: false をつけないと、at_exitフックでサーバーが起動してしまうので、地味にハマる。注意。

inhouse_app.rb
require 'sinatra/base'

class InhouseApp < Sinatra::Base
  get '/' do
    '<h1>It works!</h1>'
  end
end
config.ru
require "bundler/setup"
Bundler.require
require_relative './inhouse_app'

run InhouseApp
docker-compose.yml
version: "3"
services:
  web:
    build: .
    volumes:
      - .:/sinatra-inhouse-app
      - ruby-bundle:/usr/local/bundle
    working_dir: /sinatra-inhouse-app
    ports:
      - 4567:4567
    command: bundle exec rackup --host 0.0.0.0 --port 4567

volumes:
  ruby-bundle:
    driver: local

起動してみる

$ docker-compose up
Creating sinatra-inhouse-app_web_1 ... done
Attaching to sinatra-inhouse-app_web_1
web_1  | [2020-10-07 14:19:22] INFO  WEBrick 1.6.0
web_1  | [2020-10-07 14:19:22] INFO  ruby 2.7.0 (2019-12-25) [x86_64-linux-musl]
web_1  | [2020-10-07 14:19:22] INFO  WEBrick::HTTPServer#start: pid=1 port=4567

image.png

GitHub認証に対応する

今はまだハローワールド。
それでも、社内サービスなので世の中に見られてはいけない。(とする。)

OAuthアプリケーション登録

https://github.com/settings/developers からアプリケーション登録をして、Client IDとSecretをもらってくる。

image.png

Authorization callback URL だけが重要で、あとはてきとうでいい。

貼り付けた画像_2020_10_07_23_24.png
↑これをメモる。

.envrc にメモる

direnv が便利なので、それを使う。↑でメモったやつをそのまま書く。

.envrc
export GITHUB_CLIENT_ID=eb12427ce9d12604f813
export GITHUB_CLIENT_SECRET=04d6af0463f16a807d7faca9ead406e76ad8192992c022ee

ちなみに、このファイルは絶対にGit管理してはいけない。
(もちろん、Qiitaに本物のIDを書いちゃダメなので、↑はサンプル。)

.gitignore
/.envrc
$ direnv allow .

あとは、docker-compose.ymlに environment を追加すれば、コンテナ側で管渠変数が見れる。

docker-compose.yml
diff --git a/docker-compose.yml b/docker-compose.yml
index b317508..2d75301 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,6 +6,9 @@ services:
       - .:/sinatra-inhouse-app
       - ruby-bundle:/usr/local/bundle
     working_dir: /sinatra-inhouse-app
+    environment:
+      - GITHUB_CLIENT_ID
+      - GITHUB_CLIENT_SECRET
     ports:
       - 4567:4567
     command: bundle exec rackup --host 0.0.0.0 --port 4567
$ docker-compose run --rm web sh
/sinatra-inhouse-app # echo $GITHUB_CLIENT_ID
eb12427ce9d12604f813

omniauth-githubを入れる

OmniAuthをSinatraに組み込む方法はここに書いてある。
https://github.com/omniauth/omniauth/wiki/Sinatra-Example

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

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

gem "sinatra", require: false

gem "omniauth"
gem "omniauth-github"
inhouse_app.rb
diff --git a/inhouse_app.rb b/inhouse_app.rb
index b2621c7..1716520 100644
--- a/inhouse_app.rb
+++ b/inhouse_app.rb
@@ -1,7 +1,18 @@
 require 'sinatra/base'

 class InhouseApp < Sinatra::Base
+  use Rack::Session::Cookie
+  use OmniAuth::Builder do
+    provider :github, ENV['GITHUB_CLIENT_ID'], ENV['GITHUB_CLIENT_SECRET']
+  end
+
   get '/' do
     '<h1>It works!</h1>'
   end
+
+  get '/auth/github/callback' do
+    auth_hash = request.env['omniauth.auth']
+    puts auth_hash
+    redirect to('/')
+  end
 end

動作確認

image.png

貼り付けた画像_2020_10_07_23_41.png

貼り付けた画像_2020_10_07_23_42.png

image.png

こんなかんじで、あとはログを見ると

I, [2020-10-07T14:45:12.104513 #1]  INFO -- omniauth: (github) Request phase initiated.
I, [2020-10-07T14:45:12.531651 #1]  INFO -- omniauth: (github) Callback phase initiated.
#<OmniAuth::AuthHash credentials=#<OmniAuth::AuthHash expires=false token="???????????????????"> extra=#<OmniAuth::AuthHash all_emails=#<Hashie::Array []> raw_info=#<OmniAuth::AuthHash avatar_url="https://avatars2.githubusercontent.com/u/11763113?v=4" bio=nil blog="http://yusukeiwaki.hatenablog.com/" company=nil created_at="2015-04-02T00:02:12Z" email=nil events_url="https://api.github.com/users/YusukeIwaki/events{/privacy}" followers=9 followers_url="https://api.github.com/users/YusukeIwaki/followers" following=19 following_url="https://api.github.com/users/YusukeIwaki/following{/other_user}" gists_url="https://api.github.com/users/YusukeIwaki/gists{/gist_id}" gravatar_id="" hireable=nil html_url="https://github.com/YusukeIwaki" id=11763113 location="Fukuoka, Japan" login="YusukeIwaki" name="Yusuke Iwaki" node_id="MDQ6VXNlcjExNzYzMTEz" organizations_url="https://api.github.com/users/YusukeIwaki/orgs" public_gists=32 public_repos=147 received_events_url="https://api.github.com/users/YusukeIwaki/received_events" repos_url="https://api.github.com/users/YusukeIwaki/repos" site_admin=false starred_url="https://api.github.com/users/YusukeIwaki/starred{/owner}{/repo}" subscriptions_url="https://api.github.com/users/YusukeIwaki/subscriptions" twitter_username=nil type="User" updated_at="2020-10-07T14:42:15Z" url="https://api.github.com/users/YusukeIwaki"> scope=""> info=#<OmniAuth::AuthHash::InfoHash email=nil image="https://avatars2.githubusercontent.com/u/11763113?v=4" name="Yusuke Iwaki" nickname="YusukeIwaki" urls=#<OmniAuth::AuthHash Blog="http://yusukeiwaki.hatenablog.com/" GitHub="https://github.com/YusukeIwaki">> provider="github" uid="11763113">

こんなのがでているはず。

ハローワールドを認証つきにする

ここに詳しく書いてある。
http://sinatrarb.com/faq.html#auth

認証されていなかったらGitHubログインの方に飛ばし、
許可のないユーザであればForbidden表示をする。

inhouse_app.rb
diff --git a/inhouse_app.rb b/inhouse_app.rb
index 1716520..29726af 100644
--- a/inhouse_app.rb
+++ b/inhouse_app.rb
@@ -6,13 +6,38 @@ class InhouseApp < Sinatra::Base
     provider :github, ENV['GITHUB_CLIENT_ID'], ENV['GITHUB_CLIENT_SECRET']
   end

+  helpers do
+    def authorize!
+      unless authenticated?
+        redirect to('/auth/github')
+      end
+      unless authorized?
+        halt 403, 'Forbidden. <a href="/logout">Logout</a>'
+      end
+    end
+
+    def authenticated?
+      session[:user]
+    end
+
+    def authorized?
+      session[:user] == 'YusukeIwaki'
+    end
+  end
+
   get '/' do
+    authorize!
     '<h1>It works!</h1>'
   end

   get '/auth/github/callback' do
     auth_hash = request.env['omniauth.auth']
-    puts auth_hash
+    session[:user] = auth_hash['extra']['raw_info']['login']
+    redirect to('/')
+  end
+
+  get '/logout' do
+    session[:user] = nil
     redirect to('/')
   end
 end

ためしに他のユーザでアクセスしてみると、こんなかんじ。

image.png

Herokuにデプロイする

認証が最低限かけれたところで、Herokuに上げてみる。

$ heroku create sinatra-inhouse-app-sample
Creating ⬢ sinatra-inhouse-app-sample... done
https://sinatra-inhouse-app-sample.herokuapp.com/ | https://git.heroku.com/sinatra-inhouse-app-sample.git
$ git push heroku master
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
Delta compression using up to 12 threads
Compressing objects: 100% (18/18), done.
Writing objects: 100% (21/21), 3.04 KiB | 3.04 MiB/s, done.
Total 21 (delta 7), reused 3 (delta 0), pack-reused 0
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Ruby app detected
remote: -----> Installing bundler 2.1.4
remote: -----> Removing BUNDLED WITH version in the Gemfile.lock
remote: -----> Compiling Ruby/Rack
remote: -----> Using Ruby version: ruby-2.6.6
remote: -----> Installing dependencies using bundler 2.1.4
remote:        Running: BUNDLE_WITHOUT=development:test BUNDLE_PATH=vendor/bundle BUNDLE_BIN=vendor/bundle/bin BUNDLE_DEPLOYMENT=1 bundle install -j4
remote:        Fetching gem metadata from https://rubygems.org/.......
remote:        Using bundler 2.1.4
remote:        Fetching hashie 4.1.0
remote:        Fetching multipart-post 2.1.1
remote:        Fetching jwt 2.2.2
remote:        Installing jwt 2.2.2

Herokuはとても賢いので、GemfileがあればRubyビルドを開始してくれる。

あとは、環境変数をセットして

$ heroku config:set GITHUB_CLIENT_ID=eb12427ce9d12604f813
$ heroku config:set GITHUB_CLIENT_SECRET=04d6af0463f16a807d7faca9ead406e76ad8192992c022ee

OAuthのアプリケーション設定でコールバックURLをHerokuのものに書き換えて、

image.png

これだけで、Herokuにアクセスしたら認証もしっかり動いて、いいかんじ。

image.png

おまけ: bin/consoleを使えるようにする

SinatraではRailsコンソールは使えないが、それに近いものは使えるようにしておく。

bin/console
#!/usr/bin/env ruby

require "bundler/setup"
Bundler.require
require_relative '../inhouse_app'

Pry.start
Gemfile
diff --git a/Gemfile b/Gemfile
index 4fa2b7b..aef2284 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,6 +4,7 @@ source "https://rubygems.org"

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

+gem "pry"
 gem "sinatra", require: false

 gem "omniauth"
$ docker-compose run --rm web sh
/sinatra-inhouse-app # bundle install
Fetching gem metadata from https://rubygems.org/.......
Resolving dependencies...
Using bundler 2.1.2
Using coderay 1.1.3
Using multipart-post 2.1.1
Using faraday 1.0.1
Using hashie 4.1.0
Using jwt 2.2.2
Using method_source 1.0.0
Using multi_json 1.15.0
Using multi_xml 0.6.0
Using ruby2_keywords 0.0.2
Using mustermann 1.1.1
Using rack 2.2.3
Using oauth2 1.4.4
Using omniauth 1.9.1
Using omniauth-oauth2 1.7.0
Using omniauth-github 1.4.0
Fetching pry 0.13.1
Installing pry 0.13.1
Using rack-protection 2.1.0
Using tilt 2.0.10
Using sinatra 2.1.0
Bundle complete! 4 Gemfile dependencies, 20 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

/sinatra-inhouse-app # bin/console
[1] pry(main)> InhouseApp
=> InhouseApp
[2] pry(main)> InhouseApp.run
=> false
[3] pry(main)> exit

中間おさらい

ここまでで

  • Docker, docker-composeで開発環境はサッと作れるのがよい。
  • Herokuにデプロイしたい。
  • GitHub認証で、なおかつ知らないユーザは弾きたい。

については比較的簡単に作れた。

ここまでだと「あれ?Sinatraって結構べんりじゃね?」と錯覚する。

そう、錯覚。

なぜなら、ここまではRailsで作ったとしても、ほとんどRailsのフレームワークの機能を使わないからだ。

フォーム入力したらそれに沿って外部サービスのAPIを叩きに行って、結果を表示するだけのアプリ。一応ActiveRecordは使用する。
ただし社内サービスなので、フォームバリデーションとかは適当でいい。あとは...

後半は、本体を作り始める。

ActiveRecordを使う

ここから徐々にSinatraのつらみがでてくる。

Railsであればconfig/database.ymlをいじって、app/models/ にクラスを追加すれば、DBアクセスができる状態になるが、Sinatraはsinatra-activerecordなどの設定が必要だ。

docker-composeにpostgresqlを追加する

ローカル環境でもpostgresqlを動かす。手順は「postgresql docker-compose」とかでググれば出てくるだろう。

docker-compose.yml
diff --git a/docker-compose.yml b/docker-compose.yml
index 2d75301..26f4207 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,12 +7,28 @@ services:
       - ruby-bundle:/usr/local/bundle
     working_dir: /sinatra-inhouse-app
     environment:
+      - POSTGRES_HOST=postgresql
+      - POSTGRES_USER=dbuser
+      - POSTGRES_PASSWORD=dbpassword
       - GITHUB_CLIENT_ID
       - GITHUB_CLIENT_SECRET
+    depends_on:
+      - postgresql
     ports:
       - 4567:4567
     command: bundle exec rackup --host 0.0.0.0 --port 4567

+  postgresql:
+    image: postgres:12-alpine
+    restart: always
+    environment:
+      POSTGRES_USER: dbuser
+      POSTGRES_PASSWORD: dbpassword
+    volumes:
+      - postgresql-data:/var/lib/postgresql/data
+
 volumes:
   ruby-bundle:
     driver: local
+  postgresql-data:
+    driver: local
$ docker-compose pull

これで、postgresqlがdocker-compose upやdocker-compose run で起動するようになる。

sinatra-activerecordを入れる

Gemfileに追記。
あと、postgresql関連のライブラリを入れるために(bundle installを通すために)Dockerfileで build-baseとpostgresql-devのインストールプロセスを追加する必要がある。
lessはPryの標準出力が長くなってもバグらないように追加。

Gemfile追加
diff --git a/Dockerfile b/Dockerfile
index 5ac8708..ab0beb4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1 +1,6 @@
 FROM ruby:2.7-alpine
+
+RUN apk add --no-cache \
+    build-base \
+    less \
+    postgresql-dev
diff --git a/Gemfile b/Gemfile
index aef2284..19d0a62 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,8 +4,12 @@ source "https://rubygems.org"

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

+gem "activerecord"
+gem "pg"
 gem "pry"
+gem "rake"
 gem "sinatra", require: false
+gem "sinatra-activerecord"

 gem "omniauth"
 gem "omniauth-github"

rake db:create

sinatra-activerecordのREADMEにある説明から、雰囲気で設定ファイルなどを追加する。

config/database.yml については、Railsのものをそのまま使える。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: <%= ENV.fetch("POSTGRES_HOST") { '127.0.0.1' } %>
  port: 5432
  username: <%= ENV["POSTGRES_USER"] %>
  password: <%= ENV["POSTGRES_PASSWORD"] %>

development:
  <<: *default
  database: inhouse_app  

test:
  <<: *default
  database: inhouse_app_test  

production:
  url: <%= ENV['DATABASE_URL'] %>
Rakefile
require "sinatra/activerecord/rake"

namespace :db do
  task :load_config do
    require "bundler/setup"
    Bundler.require
    require "./inhouse_app"
  end
end
/sinatra-inhouse-app # bundle exec rake -T
rake db:create              # Creates the database from DATABASE_URL or config/database.yml for the current RAILS_E...
rake db:create_migration    # Create a migration (parameters: NAME, VERSION)
rake db:drop                # Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV...
rake db:environment:set     # Set the environment value for the database
rake db:fixtures:load       # Loads fixtures into the current environment's database
rake db:migrate             # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)
rake db:migrate:status      # Display status of migrations
rake db:prepare             # Runs setup if database does not exist, or runs migrations if it does
rake db:rollback            # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rake db:schema:cache:clear  # Clears a db/schema_cache.yml file
rake db:schema:cache:dump   # Creates a db/schema_cache.yml file
rake db:schema:dump         # Creates a db/schema.rb file that is portable against any DB supported by Active Record
rake db:schema:load         # Loads a schema.rb file into the database
rake db:seed                # Loads the seed data from db/seeds.rb
rake db:seed:replant        # Truncates tables of each database for current environment and loads the seeds
rake db:setup               # Creates the database, loads the schema, and initializes with the seed data (use db:re...
rake db:structure:dump      # Dumps the database structure to db/structure.sql
rake db:structure:load      # Recreates the databases from the structure.sql file
rake db:version             # Retrieves the current schema version number

/sinatra-inhouse-app # bundle exec rake db:create
Created database 'inhouse_app'
Created database 'inhouse_app_test'

rake db:migrate

Railsジェネレータがない。 rails g migration できないので、代わりに rake db:create_migration を使う。

/sinatra-inhouse-app # bundle exec rake db:create_migration NAME=create_audit_log 
db/migrate/20201007162814_create_audit_log.rb

作ってしまえばRailsと同じなのだが、いざcreate_tableを手で書くと、割と迷う。

db/migrate/20201007162814_create_audit_log.rb
class CreateAuditLog < ActiveRecord::Migration[6.0]
  def up
    create_table :audit_logs do |t|
      t.string :subject, null: false
      t.text :description
      t.datetime :created_at, index: true, null: false
    end
  end

  def down
    drop_table :audit_logs
  end
end

db:migrateはRailsと同じ。

/sinatra-inhouse-app # bundle exec rake db:migrate
== 20201007162814 CreateAuditLog: migrating ===================================
-- create_table(:audit_logs)
   -> 0.0090s
== 20201007162814 CreateAuditLog: migrated (0.0091s) ==========================

モデルの作成

app/models/audit_log.rb
class AuditLog < ActiveRecord::Base
  validates :subject, presence: true
end

作っておしまい...ではない。Railsと違って、Sinatraはオートロードみたいな仕組みを持たないので、明示的にrequireしてあげないといけないのだ。

inhouse_app.rb
diff --git a/inhouse_app.rb b/inhouse_app.rb
index 5cd6ed5..a3ab821 100644
--- a/inhouse_app.rb
+++ b/inhouse_app.rb
@@ -1,4 +1,8 @@
 require 'sinatra/base'
+Dir['./app/models/**/*.rb'].each { |f| require_relative f }
+
+ActiveRecord::Base.logger = Logger.new($stdout)
+ActiveRecord::Base.verbose_query_logs = true

 class InhouseApp < Sinatra::Base
   use Rack::Session::Cookie

ここまでくれば、あとは自由にActiveRecordが使える

/sinatra-inhouse-app # bin/console
[1] pry(main)> AuditLog.last
D, [2020-10-07T16:35:50.957634 #239] DEBUG -- :   AuditLog Load (0.7ms)  SELECT "audit_logs".* FROM "audit_logs" ORDER BY "audit_logs"."id" DESC LIMIT $1  [["LIMIT", 1]]
D, [2020-10-07T16:35:50.957900 #239] DEBUG -- :   ↳ (pry):1:in `__pry__'
=> nil
[2] pry(main)> AuditLog.count
D, [2020-10-07T16:35:54.177493 #239] DEBUG -- :    (0.9ms)  SELECT COUNT(*) FROM "audit_logs"
D, [2020-10-07T16:35:54.177788 #239] DEBUG -- :   ↳ (pry):2:in `__pry__'
=> 0

アクセスカウンターを作ってHerokuにデプロイする

DBとの結合動作確認のためだけに、とりあえずアクセスカウンタを付けてみる。

inhouse_app.rb
diff --git a/inhouse_app.rb b/inhouse_app.rb
index a3ab821..498ec31 100644
--- a/inhouse_app.rb
+++ b/inhouse_app.rb
@@ -31,7 +31,13 @@ class InhouseApp < Sinatra::Base

   get '/' do
     authorize!
-    '<h1>It works!</h1>'
+
+    AuditLog.create!(
+      subject: "#{session[:user]} accessed",
+      description: "#{session[:user]} accessed on '/'",
+    )
+
+    "<h1>It works!</h1> -- #{AuditLog.count}"
   end

   get '/auth/github/callback' do

image.png

リロードするたびにカウンタが増えることを確認したら、

$ git push heroku master

で、デプロイ。データベースは勝手に追加されたりはしないので、手動で追加。

$ heroku addons:create heroku-postgresql:hobby-dev
Creating heroku-postgresql:hobby-dev on ⬢ sinatra-inhouse-app-sample... free
Database has been created and is available
 ! This database is empty. If upgrading, you can transfer
 ! data from another database with pg:copy
Created postgresql-curved-40316 as DATABASE_URL
Use heroku addons:docs heroku-postgresql to view documentation

最初はDBがマイグレーションされていないので、エラー画面になるが、驚く必要はない。

image.png

$ heroku run bundle exec rake db:migrate

上記のように、heroku runでマイグレーションを実行すれば直る。

image.png

フォームを作る

actionview まで入れてしまうと、bundle install でNokogiriが入ってくるので、「それはもはやRailsでは?」みたいな状況になる。
素のSinatraでもerbは使えるので、erbで頑張ってみる。

これが、フォームヘルパーもタグヘルパーも使えないので、割と苦行だ。

index.html.erbを読み込むには?

ERBを使うには erb っていうメソッドを使うわけだが、READMEのように erb :index と指定するとviews/index.html.erbではなく、views/index.erbを探しに行こうとする。
かといって、 erb "index.html" と指定すると、index.htmlという文字列をERBとして解釈しようとする。
なので、無理矢理でも :'index.html' のようなシンボルを指定する必要がある。

inhouse_app.rb
diff --git a/inhouse_app.rb b/inhouse_app.rb
index 498ec31..882dc60 100644
--- a/inhouse_app.rb
+++ b/inhouse_app.rb
@@ -37,7 +37,7 @@ class InhouseApp < Sinatra::Base
       description: "#{session[:user]} accessed on '/'",
     )

-    "<h1>It works!</h1> -- #{AuditLog.count}"
+    erb :'index.html'
   end

   get '/auth/github/callback' do
views/index.html.erb
<html>
<body>
<h1>It works!</h1> -- <%= AuditLog.count %>

from erb
</body>
</html>

image.png

フォームを作る

フォームオブジェクトはRailsと同様に書けるのだけど、form_for, form_withが無いので、愚直に <form action="..." method="POST"> から書かないといけない。
フォームのHTMLを手で書きたい人にはいいかもしれないが、そこまでフォームの見た目は気にしない社内サービスだと、これは結構めんどくさい。

inhouse_app.rb
diff --git a/inhouse_app.rb b/inhouse_app.rb
index 882dc60..75ecdf1 100644
--- a/inhouse_app.rb
+++ b/inhouse_app.rb
@@ -40,6 +40,22 @@ class InhouseApp < Sinatra::Base
     erb :'index.html'
   end

+  get '/profile' do
+    @form = ProfileForm.new
+    erb :'profile.html'
+  end
+
+  post '/profile' do
+    profile_form_params = params.slice(:name, :age, :address)
+    @form = ProfileForm.new(profile_form_params)
+    if @form.valid?
+      "Profile登録しにいく"
+      redirect to('/profile')
+    else
+      erb :'profile.html'
+    end
+  end
+
   get '/auth/github/callback' do
     auth_hash = request.env['omniauth.auth']
     session[:user] = auth_hash['extra']['raw_info']['login']
app/models/profile_form.rb
class ProfileForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :age, :integer
  attribute :address, :string

  validates :name, presence: true
  validates :age, presence: true
end
views/profile.html.erb
<html>
<body>
<h1>Profile</h1>

<% if @form.errors.present? %>
<ul class="errors">
<% @form.errors.full_messages.each do |error_message| %>
  <li><%= error_message %></li>
<% end %>
</ul>
<% end %>

<form action='/profile' method='POST'>
  <div>
    name: <input type="text" name="name" value="<%= @form.name %>" />
  </div>
  <div>
    age: <input type="number" name="age" value="<%= @form.age %>" />
  </div>
  <div>
    address: <input type="text" name="address" value="<%= @form.address %>" />
  </div>

  <input type="submit" value="OK" />
</form>
</body>
</html>
GET /profile POST /profile バリデーションエラー
image.png image.png

まとめ

ActiveRecordを使う・画面(とくにフォーム)がある・HerokuにデプロイするようなWebアプリケーション書くなら、たとえ単機能であってもSinatraではなくRailsを選択したほうが、フレームワークの恩恵を受けやすいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2