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
```

```Dockerfile
FROM ruby:2.7-alpine
```

### Gemfileを作る

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

## SinatraでHello Worldするまで

このへんまでは楽勝。

```rb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/6dbab0d2-2654-6e60-5b0c-0fd4dd5f795a.png)

## GitHub認証に対応する

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

### OAuthアプリケーション登録

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/37fbc474-305e-f640-8ef4-d2d764be8dfd.png)

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

![貼り付けた画像_2020_10_07_23_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/5b946e1f-1dfc-5860-2a38-4dfe7aa92002.png)
↑これをメモる。

### .envrc にメモる

[direnv](https://github.com/direnv/direnv) が便利なので、それを使う。↑でメモったやつをそのまま書く。

```bash:.envrc
export GITHUB_CLIENT_ID=eb12427ce9d12604f813
export GITHUB_CLIENT_SECRET=04d6af0463f16a807d7faca9ead406e76ad8192992c022ee
```

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

```text:.gitignore
/.envrc
```

```
$ direnv allow .
```

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

```diff: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

```ruby: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"
```

```diff: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/4930ecc9-a090-c1fb-a46f-4d461ea83bee.png)

![貼り付けた画像_2020_10_07_23_41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/f677ee12-58dd-c927-bb8d-3aec45a7f203.png)

![貼り付けた画像_2020_10_07_23_42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/7dd448fb-e2f2-c061-1a35-f843a696e75d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/93097f1a-4e9c-337d-f9d5-4e1392b5d48b.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表示をする。

```diff: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/1848a510-6b78-2314-00ae-010866f1cb1b.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/605277ab-3a5d-8636-a778-f7fbbcf9d79c.png)


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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/1a4944e8-12a5-f8bf-0caf-9fc6fe976561.png)

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

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

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

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

Pry.start
```

```diff: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」とかでググれば出てくるだろう。

```diff: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の標準出力が長くなってもバグらないように追加。

```diff: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](https://github.com/sinatra-activerecord/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'] %>
```

```rb: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してあげないといけないのだ。

```diff: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との結合動作確認のためだけに、とりあえずアクセスカウンタを付けてみる。

```diff: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/ba920b57-cb3c-e457-8c49-9c9b64910a5e.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/adfc6b33-db17-762b-7e1d-6e3df27807e3.png)

```
$ heroku run bundle exec rake db:migrate
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/c7954cf4-7ca5-e341-0f51-a7f31cb9ff1c.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'` のようなシンボルを指定する必要がある。

```diff: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/a124e665-d03a-32cf-1a06-9bdbc066e6a4.png)

### フォームを作る

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


```diff: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
```

```erb: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/dd67c47e-986f-0d98-5bd9-44b46eb15087.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/68c9cba6-efcd-0e3a-3651-7bdcb36305cf.png)|


## まとめ

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