Hanami関係の小ネタまとめ

Hanami で社内アプリの開発をやっているので、それでわかった小ネタを書きます。

## DBのタイムゾーン

DBのタイムゾーンは Sequel の設定で行います。 `config/initializers/sequel.rb` を作成して次のように設定しましょう。

```ruby
Sequel.application_timezone = :tokyo
Sequel.database_timezone = :utc
Sequel.typecast_timezone = :utc
```

DB 中では UTC、データを取り出すときは JST で使えるようになります。
参考: http://sequel.jeremyevans.net/rdoc/classes/Sequel/Timezones.html

## db rollback

Hanami で migration するときは `hanami db migrate` コマンドを実行しますが、 `hanami db rollback` はありません。
`rollback` したいときは、マイグレーションバージョンを指定して migrate します。

```
hanami db migrate 20170911231450
```

## 環境変数

Hanami では [dotenv](https://github.com/bkeepers/dotenv) を使って環境変数を読み込みます。しかし、 `.env.local` や `.env.development.local` のような `local` 系のファイルの読み込みに対応していません(今後改善されるかも: https://discourse.hanamirb.org/t/hanami-2-0-ideas/306/31 )。
また、 dotenv は Ruby の中でしか使えないので、 node で assets まわりの処理を行う場合等には環境変数を読めません。

これに関しては [direnv](https://github.com/direnv/direnv) を併用することにしました。direnv は `.envrc` に定義した環境変数を使えるようにします。これを `.env.local` の代わりに使います。 direnv であれば node からも環境変数を読み込めます。

そこで、 `HANAMI::ENV` に依存して変わる環境変数は `.env.development` と `.env.test` に定義して、それ以外は `.envrc` に定義するようにします。 `.envrc` は gitignore してそれ以外はコミットします。

```
.env.development  # コミットする
.env.test         # コミットする
.envrc            # ignore
.envrc.sample     # コミットする
```

## ActiveSupport 的なもの

https://github.com/hanami/utils にいくつか便利機能があります。

ただ、 Ruby そのものも機能が増えているので、例えば `blank?` 程度であれば `hoge&.empty?` で十分な場合も多々あります。

### ActiveSupport::Time 的なもの

時間に関する比較は https://github.com/zverok/time_math2 が Hanami の設計思想に近くて使いやすいように思いました。
ActiveSupportとの比較もあります http://zverok.github.io/blog/2016-06-27-time_math2.html

## Session の利用

Session はデフォルトでは使用できないので http://hanamirb.org/guides/1.0/actions/sessions/ を読んで使えるようにしてください。

## Rack::Test

Hanami の Controller のテストは http://hanamirb.org/guides/1.0/actions/testing/ にあるように `call` メソッドの戻り値で行うことが出来ます。ただ、 session を利用する場合、この方法では session 関係のメソッドをスタブしなければならなくなるので、わりと微妙です。そこでもう素直に Rack::Test を使います。

`Rack::Test` を動かすには次のように `Web::Application` を `app` メソッドで動かします。(test-unitの例なので minitest や rspec の場合はよしなにやってください。)

```ruby
require_relative 'test_helper'
require 'rack/test'

class RackTestHelper < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    Rack::Builder.new.run Web::Application.new
  end
end
```

## i18n

- [i18n](https://github.com/svenfuchs/i18n)
- [i18n-debug](https://github.com/fphilipe/i18n-debug) ... i18nのキーを表示するgem

上記の gem を使います。 i18n-debug はなくてもいいんですけど、あると便利です。

`config/initializers/i18n.rb` を次のように定義します。

```ruby
require 'i18n'
require 'i18n/debug' if ENV['I18N_DEBUG'] == 'true'

I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
I18n.load_path = Dir[
  Hanami.root.join('config/locales/*.yml').to_s,
  Hanami.root.join('config/locales/**/*.yml').to_s
]
I18n.backend.load_translations
I18n.enforce_available_locales = false
I18n.config.default_locale = 'ja'
```

これで `I18n.t` や `I18n.l` のようなメソッドを使えます。

i18n を便利に使えるようにヘルパーメソッドを定義します。

```ruby
# lib/locale_helper.rb
require 'hanami/utils/string'

module LocaleHelper
  def t(key, options = {})
    ::I18n.t(key, default_options(key).merge(options))
  end

  private

  def default_options(key)
    if key.start_with?('.')
      # split view class name
      app, _, controller, action = self.class.name.split('::').map {|class_name|
        Hanami::Utils::String.new(class_name).underscore
      }

      {scope: "#{app}.#{controller}.#{action}"}
    else
      {}
    end
  end
end
```

こんな感じの module を用意しておいて、 `apps/web/application.rb` で読み込みます。

```ruby
module Web
  class Application < Hanami::Application
    configure do
      view.prepare do
        include Hanami::Helpers
        include Web::Assets::Helpers
        include LocaleHelper
      end
```

これにより view の中で　`t('.submit')` のようにすると path に対応したキーを読み込んで i18n を動かします。

### バリデーションエラーの i18n

Hanami は Action で parameter のバリデーションを書けます。
 http://hanamirb.org/guides/1.0/actions/parameters/#whitelisting

しかし、実のところこの実装はちょっと微妙です。 controller の `params` メソッドでは [Hanami::Validation](https://github.com/hanami/validations) の custum predicate や i18n 機能を使えません。

controller の `params` が何をやっているかというと、 Hanami::Validation の `validations` に処理を委譲するのとほぼ同じことをやっています。

この部分の実装は https://github.com/hanami/controller/blob/v1.1.0.beta1/lib/hanami/action/validatable.rb#L97 です。

```ruby
def params(klass = nil, &blk)
  if klass.nil?
    klass = const_set(PARAMS_CLASS_NAME, Class.new(Params))
    klass.class_eval { params(&blk) }
  end

  @params_class = klass
end
```

このコードは少しわかりづらいですが、引数の `klass` を渡さない場合、 `Hanami::Action::Params.params` に引数 `blk` を委譲します。

`Hanami::Action::Params.params` の実装は https://github.com/hanami/controller/blob/v1.1.0.beta1/lib/hanami/action/params.rb#L57 で

```ruby
def self.params(&blk)
  validations(&blk || ->() {})
end
```

こうなっていて、この `validations` というのは `Hanami::Validations` の https://github.com/hanami/validations/blob/v1.1.0.beta1/lib/hanami/validations.rb#L93 この部分になります。

このメソッドは本来、

```ruby
class Signup
  include Hanami::Validations

  validations do
    required(:name).filled
  end
end
```

のように使います。
そして、 i18n の設定は、

```ruby
class Signup
  include Hanami::Validations
  messages :i18n

  validations do
    required(:name).filled
  end
end
```

このように記述します。
`Hanami::Action::Params.params` は `Hanami::Validations.validations` とほぼ同じものなので、 `params` のなかに `messages` を書くと

```ruby
class Signup
  include Hanami::Validations

  validations do
    messages :i18n

    required(:name).filled
  end
end
```

と、書いていることと同じことになり、うまく動きません。

まぁ、ちょっとどうかと思う実装なのですが、そういう仕様なので仕方がありません。
ではどうすればいいかというと、最初の `Hanami::action.params` の引数 `klass` を渡してやります。

```ruby
module Web::Controllers::Books
  class Create
    # 略
    params Class.new(Hanami::Action::Params) do
      messages :i18n

      validations do
        required(:book).schema do
          required(:title).filled(:str?)
          required(:author).filled(:str?)
        end
      end
    end
    # 略
  end
end
```

i18n設定とかはどうせどの params でも使うので、下記の issue のコメントに書いたような感じで
Hanami::Action::Paramsを継承した基底クラスを作って、設定をまとめておくと便利です。
https://github.com/hanami/controller/issues/209#issuecomment-300771890

### Interactorによるi18n

僕としては Controller で params を処理するより `Hanami::Interactor` を用いてサービス層を作る方が良いのではないかと思っています。
http://magazine.rubyist.net/?0056-hanami#l15 ここで作った Interactor を前提にして Validation の基底 module を作ります。

```ruby
require 'dry/validation/messages/i18n'

module AbstractValidation
  def self.included(klass)
    klass.class_eval do
      include Hanami::Validations

      messages :i18n
    end
  end
end
```

で、これを使って Interactor を作ります。

```ruby
# lib/bookshelf/interactors/user_Interactor/create.rb
module UserInteractor
  class Create
    class Validation
      include AbstractValidation

      validations do
        required(:email).value(:filled?)
        required(:password).value(:filled?, size?: 8..40, format?: /\A[\w!$%@#123]+\z/).confirmation
        required(:password_confirmation).filled(:str?)
      end
    end
```

## File Upload

https://github.com/katafrakt/hanami-shrine これでできます。ただ、 plugin を色々増やせるというものなので、ちょっと冗長な気もします。

## 認証関係

次のような Rack ベースの gem は普通に使えます。

- Warden
- OmniAuth

参考: https://www.monterail.com/blog/2016/hanami-with-oauth

[omniauth-hanami](https://github.com/katafrakt/omniauth-hanami) という gem もありますが、素で OmniAuth を使うのとあまり変わらないように感じたので使いませんでした。

Warden は strategy 使わないならあまり使う理由ないので session 使って自分で実装しても対して手間は変わりません。

また http://awesome-hanami.org/#awesome-hanami-hanami-gem-list-authentication-and-oauth にも認証関係の gem があります。