Ruby
Rails
DDD
hanami

Hanamiの入門ガイドを和訳しました。

はじまる前に

Hanamiはとても素晴らしいフレームワークです。
最近1.1.0にアップデートされました。
この記事はHanami公式サイトのIntroductionを和訳したものです。
辞書を引きながら書いてみましたが、間違いや足りないところが多いと思います。
ご指摘いただけると幸いです。

紹介

home-background.jpg
Hanami is a modern web framework for Ruby.

レスポンスタイムが早いです。

Hanamiは速度に最適化されました。
数ミリ秒の時間でレスポンスを返します。
あなたのアプリをより早くするためにCDN(Content Delivery Networks)の利点を最大限で取ります。

すべての機能が提供されているが、軽量です。

メモリを汚すことなくパワーフルな製品を作るために我々が提供する100個以上の機能を使います。
Hanamiは、他のfull-featured Ruby framework1より60%程度少ないメモリーを消費します。

最初から基本的に安全です。

もっとも普遍的なセキュリティの脅威に対してあなたのユーザーを保護するためにContent Security Policy2、X-Frame headers3、automatic escaping4のような最新のブラウザ技術を使用してアプリケーションを配布します。

シンプルな上に、生産力が高いです。

Hanamiのデザインのシンプルさとコンポーネントのミニマルリズムをお楽しみにしてください。
数分で柔軟なコードが作成し、将来それを簡単に変更できます。

前書き

Hanamiとは何ですか?

Hanamiは、多くのマイクロライブラリで構成されたRuby MVC Webフレームワークです。
Hanamiは、シンプルで安定したAPIと最小限のDSLを持ち、多くの責任を負って複雑すぎるクラスを上回る魔法のようなプレーンオブジェクトの使用を優先します。

明確な責任を持つ単純なオブジェクトを使用することの自然な影響は、ボイラープレートコードのほうが多いです。5
Hanamiは、基本的な実装を維持しながら、余計な仕事を軽減する方法を提供します。

Hanamiを選ぶ理由

Hanamiを選ぶ理由は3つあります。

Hanamiは軽量です。

Hanamiのコードは比較的短いです。
実装に関係なくすべてのWebアプリケーションが必要とすることのみに関心があります。

Hanamiにはいくつかのオプションモジュールが付属しており、他のライブラリも簡単に組み込むことができます。

Hanamiはアーキテクチャ的に堅実です。

あなたが "Railsの方法"に反していると感じたことがあるなら、あなたはHanamiに感謝することになるでしょう。6

Hanamiはコントローラーのアクションをクラスベースに保ち、独立したテストをしやすくします。

また、ユースケース・オブジェクト(interactors)にアプリケーション・ロジックを書くことをお勧めします。

ビューはテンプレートから分離されているため、内部のロジックをよく含めた独立したテストができます。

Hanamiはスレッドセーフです。

スレッドを使って作るのは、アプリケーションのパフォーマンスを向上させるのに最適な方法です。
スレッドセーフなコードを書くのは難しいことではありません。
Hanami(フレームワーク全体かその一部であろうと)はランタイムでスレッドセーフです。

ガイド

このガイドでは、高レベルのHanamiコンポーネントと、フルスタックアプリケーションで構成、使用、テストする方法について説明します。
私たちが言及する架空の製品は、"Bookshelf"と呼ばれます。(読書を共有して本を買うオンラインコミュニティ)

私たちは、入門ガイドを提供します。それでは、Hanamiで我々の最初のアプリケーションを作ります。

入門ガイド

こんにちは。あなたがこのページを読んでいるなら、あなたはHanamiについてもっと知りたいと思うでしょう。
それは素晴らしいことです。おめでとう!
メンテナンス可能で、安全で、高速でテスト可能なWebアプリケーションを構築するための新しい方法を探しているなら、心配しなくても大丈夫です。

Hanamiはあなたのような人のために作られています。

私はあなたが初心者か経験豊かな開発者であろうと、この学習プロセスは難しいと伝えます。
時間の経過とともに、私たちは物事がどのようにすべきかについて期待します。
そして、変化するのは苦しくなる場合があります。
しかし、変化なしに、チャレンジはなく、チャレンジなしに、成長はありません。

機能が正しく表示されない場合もありますが、それはあなたのことを意味するものではありません。
それは形成された習慣の問題、設計の誤り、またはバグである可能性があります。

私自身とコミュニティの残りの人々は、毎日Hanamiをより良くするために最善の努力を尽くしています。

このガイドでは、最初のHanamiプロジェクトを立ち上げ、シンプルな「Bookshelf」というWebアプリケーションを構築します。
Hanamiフレームワークの主要なコンポーネントすべてに触れてみましょう。すべてがテストによってガイドされます。

あなたが一人だと感じるなら、または失敗して不満を感じるなら、あきらめないで、チャットに飛び込んで助けを求めてください。
喜んであなたの話を聞いてくれる誰かがいます。

お楽しみに、
ルカ・グイディ
Hanamiのクリエイター

前提条件

はじまる前に、いくつかの前提条件があります。
まず、Webアプリケーション開発の基礎知識を持っていると仮定します。

また、Bundler、Rake、ターミナルの操作、Model、View、Controllerのパラダイムを使ったアプリケーションの構築にも精通している必要があります。

最後に、このガイドではSQLiteデータベースを使用します。
あなたが一人でついていく場合は、あなたのシステムにRuby 2.3+とSQLite 3+が正しくインストールされているのか確認してください。

新しいHanamiプロジェクトを生成する。

新しいHanamiプロジェクトを生成するには、RubygemsからHanami Gemをインストールする必要があります。
次に、新しいプロジェクトを生成するために、実行可能な新しいhanami(コマンド)を使えます。

% gem install hanami
% hanami new bookshelf

デフォルトで、プロジェクトはSQLiteデータベースを使用するようにセットアップされます。
実際のプロジェクトでは、エンジンを指定することができます。
% hanami new bookshelf --database=postgres

現在のロケーションにbookshelfという新しいディレクトリが生成されます。
それが何を含んでいるか見てみましょう。

% cd bookshelf
% tree -L 1
.
├── Gemfile
├── Rakefile
├── apps
├── config
├── config.ru
├── db
├── lib
├── public
└── spec

6 directories, 3 files

ここに私たちが知る必要があるものがあります。

  • Gemfileは、Rubygemsの依存関係を(Bundlerを使用して)定義します。
  • Rakefileは、Rakeのタスクについて記述します。
  • appsは、Rackと互換性のある1つ以上のWebアプリケーションを含めています。 ここで最初に生成されたWebと呼ばれるHanamiアプリケーションを見つけることができます。 コントローラー、ビュー、ルート、テンプレートを見つける場所です。
  • configは、設定ファイルが含まれています。
  • config.ruは、Rackサーバー用です。
  • dbは、私たちのデータベーススキーマとマイグレーションが含まれています。
  • libは、エンティティとリポジトリを含むビジネスロジックとドメインモデルを含んでいます。
  • publicは、コンパイルされた静的資産を含みます。
  • specは、私たちのテストを含んでいます。

先に進めましょう。
Bundlerを使ってgemの依存関係をインストールします。
その後、開発サーバーを立ち上げることができます。

% bundle install
% bundle exec hanami server

そして、http://localhost:2300 であなたの最初のHanamiプロジェクトの栄光を満喫してください!
このような画面が表示されるはずです。

welcome-page.png

Hanamiのアーキテクチャ

Hanamiのアーキテクチャは、複数のHanami(およびRack)アプリケーションを同じRubyプロセスの中でホストできます。

これらのアプリケーションは、apps/の配下にあります。
それらは、我々の製品になることができます。
例えば、ユーザー向けのWebインターフェイス、管理ペイン、メトリック、HTTP APIなど。

これらのパーツすべては、libs/の配下にあるビジネスロジックのための配信メカニズムです。
これは、モデルが定義されている場所であり、我々の製品が提供する機能を構成するために相互作用します。

Hanamiのアーキテクチャは、クリーンアーキテクチャに大きな影響を受けています。7

最初のテストを書く。

私たちがアプリを見ているときに見えるオープニング画面は、定義されたルートがないときに表示されるデフォルトのページです。

Hanamiは、Webアプリケーションを作成する方法としてビヘイビア駆動開発(BDD)を推奨しています。
最初のカスタムページを表示するために、高度な機能テストを作成します。

spec/web/features/visit_home_spec.rb
require 'features_helper'

describe 'Visit home' do
  it 'is successful' do
    visit '/'

    page.body.must_include('Bookshelf')
  end
end

Hanamiは、革新的なビヘイビア駆動開発ワークフローを用意しているが、特定のテストフレームワークに縛られることはなく、特別なインテグレーションやライブラリも付属していません。

私たちはここでMinitest(これがデフォルト)で進みます。
しかし、--test=rspecというオプションでプロジェクトを生成することでRSpecを使えます。
Hanamiはそのためのヘルパーとスタブファイルを生成します。

データベースのURLを微調整する必要がある場合は、.env.testをチェックしてください。

次のコマンドを実行して、テストデータベースでスキーマをマイグレートする必要があります。

% HANAMI_ENV=test bundle exec hanami db prepare

ご覧のとおり、使う環境について我々のコマンドにHANAMI_ENVという環境変数を指定します。

リクエストに続いて

今私たちはテストを持っていますが、それが失敗することがわかります。

% rake test
Run options: --seed 44759

# Running:

F

Finished in 0.018611s, 53.7305 runs/s, 53.7305 assertions/s.

  1) Failure:
Homepage#test_0001_is successful [/Users/hanami/bookshelf/spec/web/features/visit_home_spec.rb:6]:
Expected "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Not Found</title>\n  </head>\n  <body>\n    <h1>Not Found</h1>\n  </body>\n</html>\n" to include "Bookshelf".

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

さあ、それを通らさせましょう。このテストをパスするために必要なコードを追加します。段階的に。

最初に追加する必要があるのはルートです。

apps/web/config/routes.rb
root to: 'home#index'

homeコントローラーのindexアクションにアプリケーションのルートURLを指し示します。
詳細については、ルーティングガイドを参照してください。
これでindexアクションを作成できます。

apps/web/controllers/home/index.rb
module Web::Controllers::Home
  class Index
    include Web::Action

    def call(params)
    end
  end
end

これは、ビジネスロジックを実装していない空のアクションです。
各アクションには対応するビューがあります。
そして、これはRubyオブジェクトであり、リクエストを完了するために追加される必要があります。

apps/web/views/home/index.rb
module Web::Views::Home
  class Index
    include Web::View
  end
end

...それは空であり、テンプレートをレンダリングする以外に何もしません。
これは、テストをパスするために修正する必要のあるファイルです。
私たちがする必要があるのは、bookshelfという見出しを追加することだけです。

apps/web/templates/home/index.html.erb
<h1>Bookshelf</h1>

変更を保存し、テストをもう一度実行すると、テストはパスします。素晴らしい!

Run options: --seed 19286

# Running:

.

Finished in 0.011854s, 84.3600 runs/s, 168.7200 assertions/s.

1 runs, 2 assertions, 0 failures, 0 errors, 0 skips

新しいアクションの生成

新しいアクションを追加するためにHanamiの主要コンポーネントについて新しい知識を使いましょう。
Bookshelfプロジェクトの目的は書籍を管理することです。

私たちはデータベースに書籍を保存します。
そして、ユーザーが我々のプロジェクトでそれらを管理するようにします。
最初のステップは、システム内のすべての書籍のリストを表示することです。

私たちが達成したいことを記述する新しい機能テストを書いてみましょう。

spec/web/features/list_books_spec.rb
require 'features_helper'

describe 'List books' do
  it 'displays each book on the page' do
    visit '/books'

    within '#books' do
      assert page.has_css?('.book', count: 2), 'Expected to find 2 books'
    end
  end
end

このテストは簡単で十分ですが失敗します。
何故なら、/booksのURLが現在我々のアプリケーションで認識されていないからです。
それを修正するために新しいコントローラーアクションを作成します。

Hanamiのジェネレータ

Hanamiは新しい機能を追加するとき、関わらせるコードの一部のタイピングを節約するために様々なジェネレータを持っています。
ターミナルで次のものを入力してください。

% bundle exec hanami generate action web books#index

Webアプリケーションのbooksコントローラーに新しいアクションindexが生成されます。
ジェネレータは我々に空のアクション、ビュー、テンプレートを提供します。
また、デフォルトルートを次の場所に追加します。

apps/web/config/routes.rb
get '/books', to: 'books#index'

あなたがZSHを使用している場合、あなたは次のようなメッセージを見るかもしれません。

zsh: no matches found: books#index

その場合は、次のものを使えます。

% hanami generate action web books/index

我々のテストをパスするために、新しく生成されたテンプレートファイルを修正する必要があります。

apps/web/templates/books/index.html.erb
<h1>Bookshelf</h1>
<h2>All books</h2>

<div id="books">
  <div class="book">
    <h3>Patterns of Enterprise Application Architecture</h3>
    <p>by <strong>Martin Fowler</strong></p>
  </div>

  <div class="book">
    <h3>Test Driven Development</h3>
    <p>by <strong>Kent Beck</strong></p>
  </div>
</div>

変更を保存し、テストがパスされるのを確認してください!
出力にはスキップされたテストも含まれています。
これはspec/web/views/book/index_spec.rbの中に自動生成されたテストによるものです。

コントローラーとアクションの用語は混乱するかもしれないので、これを明確にしましょう。
アクションは、Hanamiアプリケーションの基礎を成しています。
コントローラーは、複数のアクションをまとめてグループ化する単なるモジュールです。
したがって、 "コントローラー"は概念的にプロジェクトに存在しますが、実際にはアクションだけを扱います。

私たちはアプリケーションで新しいエンドポイントを生成するためにジェネレータを使用しました。
しかし、あなたが気づいているかもしれないことは、新しいテンプレートがhome/index.html.erbテンプレートと同じ<h1>を含めていることです。
それを修正しましょう。

レイアウト

すべての単一のテンプレートで同じコードを繰り返すことを避けるために、レイアウトを使えます。
apps/web/templates/application.html.erbを開き、次のように修正します。

apps/web/templates/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Bookshelf</title>
    <%= favicon %>
  </head>
  <body>
    <h1>Bookshelf</h1>
    <%= yield %>
  </body>
</html>

これで、他のテンプレートから重複するコードを削除することができます。
全て正常に動作していることをチェックするためにテストをもう一度実行してみましょう。

レイアウトは、他のテンプレートのようなものですが、通常のテンプレートをラップするために使用されます。
このyield行は、通常のテンプレートの内容に置き換えられます。
レイアウトは、繰り返しのヘッダーとフッターを配置するために最適な場所です。

エンティティによるデータのモデリング

私たちのテンプレートの中に書籍をハードコーディングすることは、間違いなく、不正行為と同じです。8
アプリケーションに動的なデータを追加しましょう。

私たちはデータベースに書籍を保存し、それらをページ上に表示します。
そうするために、データベースを読み書きする方法が必要です。
エンティティとリポジトリについて説明します。

  • エンティティは、一意にそのIDで識別されるドメインオブジェクト(例えばBook)です。
  • リポジトリは、エンティティとパーシスタンス層を仲介します。

エンティティは完全にデータベースを気づいていません。
これにより、軽量で簡単なテストができます。

このような訳で、Bookに依存するデータを保持するためにリポジトリが必要です。
エンティティとリポジトリの詳細については、モデルガイドを参照してください。

Hanamiはモデル用のジェネレータを同梱していますので、それを使ってBookエンティティと対応するリポジトリを作成して見ましょう。

% bundle exec hanami generate model book
create  lib/bookshelf/entities/book.rb
create  lib/bookshelf/repositories/book_repository.rb
create  db/migrations/20161115110038_create_books.rb
create  spec/bookshelf/entities/book_spec.rb
create  spec/bookshelf/repositories/book_repository_spec.rb

ジェネレータは、エンティティ、リポジトリ、マイグレーション、および付随するテストファイルを提供します。

データベーススキーマを変更するためのマイグレーション

titleとauthorフィールドを含めるために生成されたマイグレーションを修正して見ましょう。
(マイグレーションのpathは異なります。何故なら、それはtimestampを含めているからです。)

db/migrations/20161115110038_create_books.rb
Hanami::Model.migration do
  change do
    create_table :books do
      primary_key :id

      column :title,  String, null: false
      column :author, String, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end

Hanamiは、データベーススキーマの変更を記述するDSLを提供しています。
マイグレーションがどう動くのかについて詳細はマイグレーションのガイドを参照してください。

この場合、エンティティの属性ごとにカラムを持つ新しいテーブルを定義します。
開発環境とテスト環境用にデータベースを準備しましょう。

% bundle exec hanami db prepare
% HANAMI_ENV=test bundle exec hanami db prepare

エンティティの操作

エンティティは、実際のプレーンRubyオブジェクトに本当に近いものです。
私たちはそれから欲しい振る舞いとその時になって始めてそれをどう保存するべきなのかに焦点を当てるべきです。

今のところ、単純なエンティティクラスを作成する必要があります。

lib/bookshelf/entities/book.rb
class Book < Hanami::Entity
end

このクラスは、paramsを初期化するために渡す各属性ごとにgetterとsetterを生成します。
単体テストですべてが正常に動作することを確認できます。

spec/bookshelf/entities/book_spec.rb
require 'spec_helper'

describe Book do
  it 'can be initialized with attributes' do
    book = Book.new(title: 'Refactoring')
    book.title.must_equal 'Refactoring'
  end
end

リポジトリの使用

これで、リポジトリで実行する準備が整いました。
プリロードされたアプリケーションでIRbを起動するためにHanamiのconsoleコマンドを使えます。
それで、我々はIRbの中でオブジェクトを使えます。

% bundle exec hanami console
>> repository = BookRepository.new
=> => #<BookRepository relations=[:books]>
>> repository.all
=> []
>> book = repository.create(title: 'TDD', author: 'Kent Beck')
=> #<Book:0x007f9ab61c23b8 @attributes={:id=>1, :title=>"TDD", :author=>"Kent Beck", :created_at=>2016-11-15 11:11:38 UTC, :updated_at=>2016-11-15 11:11:38 UTC}>
>> repository.find(book.id)
=> #<Book:0x007f9ab6181610 @attributes={:id=>1, :title=>"TDD", :author=>"Kent Beck", :created_at=>2016-11-15 11:11:38 UTC, :updated_at=>2016-11-15 11:11:38 UTC}>

Hanamiリポジトリは、データベースから一つ以上のエンティティを読み込むメソッドを持っています。
そして、レコードを生成することや既存のレコードを更新します。
リポジトリはカスタムクエリを実装するために新しいメソッドを定義する場所でもあります。

簡単にまとめてHanamiがエンティティとリポジトリを使用してデータをモデル化する方法を見てきました。
エンティティは行動を表します。もう一方、リポジトリはデータストアへエンティティを交換するためにマッピングを使います。
私たちはデータベーススキーマに変更を適用するマイグレーションを使えます。

動的データの表示

新しい経験のデータモデリングを使用して、書籍リストページで動的データを表示する作業を行うことができます。
先ほど作成した機能テストを調整しましょう。

spec/web/features/list_books_spec.rb
require 'features_helper'

describe 'List books' do
  let(:repository) { BookRepository.new }
  before do
    repository.clear

    repository.create(title: 'PoEAA', author: 'Martin Fowler')
    repository.create(title: 'TDD',   author: 'Kent Beck')
  end

  it 'displays each book on the page' do
    visit '/books'

    within '#books' do
      assert page.has_css?('.book', count: 2), 'Expected to find 2 books'
    end
  end
end

私たちはテストで必要なレコードを作成し、正しい数のbookクラスをページにアサートします。
私たちがこのテストを実行するとき、テストは通されるはずです。
テストが通されない場合は、テストデータベースがマイグレーションされなかった可能性があります。

テンプレートを変更して静的HTMLを削除することができます。
私たちのビューは、利用可能なすべてのレコードをループしてレンダリングする必要があります。
この変更を強制するためにテストを作成しましょう。

spec/web/views/books/index_spec.rb
require 'spec_helper'
require_relative '../../../../apps/web/views/books/index'

describe Web::Views::Books::Index do
  let(:exposures) { Hash[books: []] }
  let(:template)  { Hanami::View::Template.new('apps/web/templates/books/index.html.erb') }
  let(:view)      { Web::Views::Books::Index.new(template, exposures) }
  let(:rendered)  { view.render }

  it 'exposes #books' do
    view.books.must_equal exposures.fetch(:books)
  end

  describe 'when there are no books' do
    it 'shows a placeholder message' do
      rendered.must_include('<p class="placeholder">There are no books yet.</p>')
    end
  end

  describe 'when there are books' do
    let(:book1)     { Book.new(title: 'Refactoring', author: 'Martin Fowler') }
    let(:book2)     { Book.new(title: 'Domain Driven Design', author: 'Eric Evans') }
    let(:exposures) { Hash[books: [book1, book2]] }

    it 'lists them all' do
      rendered.scan(/class="book"/).count.must_equal 2
      rendered.must_include('Refactoring')
      rendered.must_include('Domain Driven Design')
    end

    it 'hides the placeholder message' do
      rendered.wont_include('<p class="placeholder">There are no books yet.</p>')
    end
  end
end

表示する書籍がない場合、indexページには単純なプレースホルダメッセージが表示されるように指定します。
存在する場合は、それらのすべてをリストします。
一部のデータを含むビューをレンダリングすることは、比較的簡単です。
Hanamiは、独立したテストが容易な最小限のインターフェースを備えたシンプルなオブジェクトで設計されていますが、依然として大きな仕事をしています。

これらの要件を実装するためにテンプレートを書き直してみましょう。

apps/web/templates/books/index.html.erb
<h2>All books</h2>

<% if books.any? %>
  <div id="books">
    <% books.each do |book| %>
      <div class="book">
        <h2><%= book.title %></h2>
        <p><%= book.author %></p>
      </div>
    <% end %>
  </div>
<% else %>
  <p class="placeholder">There are no books yet.</p>
<% end %>

私たちの機能テストを今実行すると、失敗することがわかります。
何故なら、コントローラーのアクションがビューにbooksを実際にexposeしてないからです。
その変更に関するテストを書けます。

spec/web/controllers/books/index_spec.rb
require 'spec_helper'
require_relative '../../../../apps/web/controllers/books/index'

describe Web::Controllers::Books::Index do
  let(:action) { Web::Controllers::Books::Index.new }
  let(:params) { Hash[] }
  let(:repository) { BookRepository.new }

  before do
    repository.clear

    @book = repository.create(title: 'TDD', author: 'Kent Beck')
  end

  it 'is successful' do
    response = action.call(params)
    response[0].must_equal 200
  end

  it 'exposes all books' do
    action.call(params)
    action.exposures[:books].must_equal [@book]
  end
end

コントローラーのアクションついてテストを書くのは基本的に2つがあります。
一つはレスポンスオブジェクト上のあるアセットです。それはRackと互換性のある状態、ヘッダーとコンテンツの配列です。
もう一つはアクションそのものです。
アクションは我々が呼び出した後、exposures9を含めるようになります。
今、アクションが:booksをexposeするように指定しました。

apps/web/controllers/books/index.rb
module Web::Controllers::Books
  class Index
    include Web::Action

    expose :books

    def call(params)
      @books = BookRepository.new.all
    end
  end
end

アクションクラスでexposeメソッドを使うことにより、@booksインスタンス変数の内容を外部に公開して、Hanamiがビューにそれを渡すことができます。
もう大丈夫です。全てのテストがもう一度通るのに十分です。

% bundle exec rake
Run options: --seed 59133

# Running:

.........

Finished in 0.042065s, 213.9543 runs/s, 380.3633 assertions/s.

6 runs, 7 assertions, 0 failures, 0 errors, 0 skips

レコードを生成するフォームを作る。

最後に残っているステップの1つは、新しい書籍をシステムに追加することです。
計画は簡単です。詳細を入力するためのフォームを持つページを作成します。

ユーザーがフォームを送信するとき、新しいエンティティを作成して保存します。
そして、ユーザーをbookリストに戻します。
テストで表現されたこの話は次のとおりです。

spec/web/features/add_book_spec.rb
require 'features_helper'

describe 'Add a book' do
  after do
    BookRepository.new.clear
  end

  it 'can create a new book' do
    visit '/books/new'

    within 'form#book-form' do
      fill_in 'Title',  with: 'New book'
      fill_in 'Author', with: 'Some author'

      click_button 'Create'
    end

    current_path.must_equal('/books')
    assert page.has_content?('New book')
  end
end

フォームの基礎を築く。

そろそろ、アクション、ビュー、テンプレートの作業に精通している必要があります。

私たちは少し作業をスピードアップして、すぐに良いところまで行けます。
まず、「New Book」ページの新しいアクションを作成します。

% bundle exec hanami generate action web books#new

これでアプリに新しいルートが追加されます。

apps/web/config/routes.rb
get '/books/new', to: 'books#new'

興味深いちょっとした部分が新しいテンプレートになります。
何故ならBookエンティティの周りにHTMLフォームを構築するためにHanamiのフォームビルダを使うようとしているからです。

フォームヘルパーの使用

フォームヘルパーを使ってこのフォームを作成しましょう。

apps/web/templates/books/new.html.erb
<h2>Add book</h2>

<%=
  form_for :book, '/books' do
    div class: 'input' do
      label      :title
      text_field :title
    end

    div class: 'input' do
      label      :author
      text_field :author
    end

    div class: 'controls' do
      submit 'Create Book'
    end
  end
%>

フォームフィードについて<label/>タグを追加し、HanamiのHTMLビルダーヘルパーを使用して<div/>コンテナの中に各フィールドをラップしました。

フォームを送信する。

私たちのフォームを送信するためには、さらに別のアクションが必要です。
Books::Createアクションを作成しましょう。

% bundle exec hanami generate action web books#create

これでアプリに新しいルートが追加されます。

apps/web/config/routes.rb
post '/books', to: 'books#create'

Createアクションを実装

私たちのbooks#createアクションは2つの事をする必要があります。
単体テストとしてそれらを表現しましょう。

spec/web/controllers/books/create_spec.rb
require 'spec_helper'
require_relative '../../../../apps/web/controllers/books/create'

describe Web::Controllers::Books::Create do
  let(:action) { Web::Controllers::Books::Create.new }
  let(:params) { Hash[book: { title: 'Confident Ruby', author: 'Avdi Grimm' }] }
  let(:repository) { BookRepository.new }

  before do
    repository.clear
  end

  it 'creates a new book' do
    action.call(params)
    book = repository.last

    book.id.wont_be_nil
    book.title.must_equal params.dig(:book, :title)
  end

  it 'redirects the user to the books listing' do
    response = action.call(params)

    response[0].must_equal 302
    response[1]['Location'].must_equal '/books'
  end
end

これらのテストを通されるのは簡単です。
エンティティをデータベースに書き込む方法をすでに見てきました。
redirectionを実装するためにredirect_toを使えます。

apps/web/controllers/books/create.rb
module Web::Controllers::Books
  class Create
    include Web::Action

    def call(params)
      BookRepository.new.create(params[:book])

      redirect_to '/books'
    end
  end
end

この最小限の実装で、テストがパスします。

% bundle exec rake
Run options: --seed 63592

# Running:

...............

Finished in 0.081961s, 183.0142 runs/s, 305.0236 assertions/s.

12 runs, 14 assertions, 0 failures, 0 errors, 2 skips

おめでとう!

バリデーションによるフォームの保護

まだ終わっていません。落ち着いてください!
本当に堅牢なフォームを構築するためには、特別な手段が必要です。
ユーザーが値を入力せずにフォームを送信するとどうなるか想像してみてください。

私たちはデータベースに不良データを埋め込むか、データの完全性違反の例外を見れます。
無効なデータからシステムを守る方法が必要です。

テストで私たちのバリデーションを表現するために、我々は疑問する必要があります。
バリデーションが失敗したら、どんなことが起こるのか?
1つの選択肢は、books#newフォームを再レンダリングすることであり、ユーザに正しく記入するために別のショットを与えることができます。
単体テストとしてこの動作を指定しましょう。

spec/web/controllers/books/create_spec.rb
require 'spec_helper'
require_relative '../../../../apps/web/controllers/books/create'

describe Web::Controllers::Books::Create do
  let(:action) { Web::Controllers::Books::Create.new }
  let(:repository) { BookRepository.new }

  before do
    repository.clear
  end

  describe 'with valid params' do
    let(:params) { Hash[book: { title: 'Confident Ruby', author: 'Avdi Grimm' }] }

    it 'creates a book' do
      action.call(params)
      book = repository.last

      book.id.wont_be_nil
      book.title.must_equal params.dig(:book, :title)
    end

    it 'redirects the user to the books listing' do
      response = action.call(params)

      response[0].must_equal 302
      response[1]['Location'].must_equal '/books'
    end
  end

  describe 'with invalid params' do
    let(:params) { Hash[book: {}] }

    it 'returns HTTP client error' do
      response = action.call(params)
      response[0].must_equal 422
    end

    it 'dumps errors in params' do
      action.call(params)
      errors = action.params.errors

      errors.dig(:book, :title).must_equal  ['is missing']
      errors.dig(:book, :author).must_equal ['is missing']
    end
  end
end

今回のテストで、2つの代替シナリオを指定します。
オリジナルの正しいシナリオとバリデーションが失敗する新しいシナリオです。
テストをパスするためには、バリデーションを実装する必要があります。

エンティティにバリデーションルールを追加することもできますが、Hanamiでは入力のソースにできるだけ近いバリデーションルールを定義することも許可します。10
Hanamiのコントローラーのアクションは、受入できる入力パラメータを定義するためにparamsクラスメソッドを使えます。

このアプローチは、paramsが使用されているものをホワイトリストにします。(他のものは、信頼できないユーザ入力によるmass-assignmentの脆弱性を避けるために破棄されます)
そして、許容できる値を定義するルールを追加します。
この例では、bookのtitleとauthorについてネストされた属性が存在しなければならないことを指定しました。

バリデーションが用意されたら、エンティティの作成とredirectionを、入力パラメータが有効な場合に限定することができます。

apps/web/controllers/books/create.rb
module Web::Controllers::Books
  class Create
    include Web::Action

    expose :book

    params do
      required(:book).schema do
        required(:title).filled(:str?)
        required(:author).filled(:str?)
      end
    end

    def call(params)
      if params.valid?
        @book = BookRepository.new.create(params[:book])

        redirect_to '/books'
      else
        self.status = 422
      end
    end
  end
end

paramsが有効な場合、Bookが生成され、アクションは別のURLにリダイレクトされます。
しかし、paramsが有効でない場合、どうなりますか?

最初に、HTTPステータスコードは422(Unprocessable Entity)に設定されます。
次に、コントロールは対応するビューに渡されます。
そして、これはレンダリングするテンプレートを知る必要があります。
この場合apps/web/templates/books/new.html.erbは、フォームを再度レンダリングするために使用されます。

apps/web/views/books/create.rb
module Web::Views::Books
  class Create
    include Web::View
    template 'books/new'
  end
end

このアプローチはうまく機能します。
Hanamiのフォームビルダは、このアクションでparamsを検査し、フォームフィールドにparamsで見つかった値を設定できるほどスマートです。
ユーザーが送信する前に1つのフィールドのみを入力すると、元の入力が表示され、再度入力する必要がありません。

テストをもう一度やり直してみてください。

バリデーションエラーの表示

何かが間違ってしまったときに、ユーザーの鼻の前にフォームを押し込むのではなく、それらに期待されることのヒントを与えるべきです。
フォームを変更して、無効なフィールドに関する通知を表示しましょう。

まず、paramsがエラーを持っている場合、エラーのリストがページに表示されることを期待します。

spec/web/views/books/new_spec.rb
require 'spec_helper'
require 'ostruct'
require_relative '../../../../apps/web/views/books/new'

describe Web::Views::Books::New do
  let(:params)    { OpenStruct.new(valid?: false, error_messages: ['Title must be filled', 'Author must be filled']) }
  let(:exposures) { Hash[params: params] }
  let(:template)  { Hanami::View::Template.new('apps/web/templates/books/new.html.erb') }
  let(:view)      { Web::Views::Books::New.new(template, exposures) }
  let(:rendered)  { view.render }

  it 'displays list of errors when params contains errors' do
    rendered.must_include('There was a problem with your submission')
    rendered.must_include('Title must be filled')
    rendered.must_include('Author must be filled')
  end
end

この新しい動作を反映するために、機能仕様を更新する必要があります。

# spec/web/features/add_book_spec.rb
require 'features_helper'

describe 'Add a book' do
  # Spec written earlier omitted for brevity

  it 'displays list of errors when params contains errors' do
    visit '/books/new'

    within 'form#book-form' do
      click_button 'Create'
    end

    current_path.must_equal('/books')

    assert page.has_content?('There was a problem with your submission')
    assert page.has_content?('Title must be filled')
    assert page.has_content?('Author must be filled')
  end
end

私たちのテンプレートではparams.errors(もしあれば)をループしてフレンドリーなメッセージを表示することができます。

apps/web/templates/books/new.html.erb
<% unless params.valid? %>
  <div class="errors">
    <h3>There was a problem with your submission</h3>
    <ul>
      <% params.error_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

テストをもう一度やり直してみてください。

% bundle exec rake
Run options: --seed 59940

# Running:

..................

Finished in 0.078112s, 230.4372 runs/s, 473.6765 assertions/s.

15 runs, 27 assertions, 0 failures, 0 errors, 1 skips

ルーターの使用を改善する。

私たちが最後に改良しようとしているのは、ルーターを使用するところです。
"web"アプリケーションのroutesファイルを開きます。

apps/web/config/routes.rb
post '/books',    to: 'books#create'
get '/books/new', to: 'books#new'
get '/books',     to: 'books#index'
root              to: 'home#index'

Hanamiは、これらのRESTスタイルのルートを構築するための便利なヘルパーメソッドを提供しています。
ルーターを少し簡略化するために使用できます。

apps/web/config/routes.rb
root to: 'home#index'
resources :books, only: [:index, :new, :create]

どのようなルートが定義されているかを知るために、これを変更しました。
特別なコマンドラインタスクであるroutesを使用して最終結果を検査することができます。

% bundle exec hanami routes
     Name Method     Path                           Action

     root GET, HEAD  /                              Web::Controllers::Home::Index
    books GET, HEAD  /books                         Web::Controllers::Books::Index
 new_book GET, HEAD  /books/new                     Web::Controllers::Books::New
    books POST       /books                         Web::Controllers::Books::Create

hanami routesは、定義されたヘルパーメソッドの名前(この名前の末尾に_path又は_urlを付けてroutesヘルパーで呼び出すことができます)、許可されたHTTPメソッド、パス、最後に要求を処理するためのコントローラーアクションが表示されます。

今resourcesヘルパーメソッドが適用されたので、名前付きルートメソッドを利用できます。
我々がform_forを使ってフォームをどう作ったのか覚えてください。

# bad
<%=
  form_for :book, '/books' do
    # ...
  end
%>

テンプレートでハードコードされたパスを含めるのは愚かです。その時、我々のルートはすでに完璧でフォームが指すルートを知っています。
私たちは、我々のビューで利用できるルートヘルパーメソッドとルート特定のヘルパーメソッドに接近するアクションを使えます。

# good
<%=
  form_for :book, routes.books_path do
    # ...
  end
%>

apps/web/controllers/book/create.rbの中で同様な変更を加えることができます。

apps/web/controllers/books/create.rb
redirect_to routes.books_path

ラッピング

あなたの最初のHanamiプロジェクトを完了したことをお祝いします!

私たちが行ったことを復習しましょう。
私たちは、お互いにどう結びつけているのか理解するためにHanamiの主な仕組みを通ってリクエストを辿ってきました。
エンティティとリポジトリを使用してドメインをモデル化する方法を見てきました。
フォームの作成、データベーススキーマの維持、ユーザー入力値のバリデーションのためのソリューションを見てきました。

私たちは長い道のりを歩んできましたが、まだまだもっと探求する余地があります。
他のガイドHanamiのAPIドキュメント、そのソースコードを読んで探索してください。
そして、ブログをフォローしてください。

何よりも、すばらしいものを作ることを楽しんでください!

補足

最初、Hanamiの入門ガイドを和訳した時、interactorsがどんなものなのか知りませんでした。
ユースケースはUMLと同様ではないかという軽い気持ちで飛ばしたのが事実です。
しかし、どうにも気になって調べました。

クリーンアーキテクチャで説明しているユースケースはUMLのユースケースと意味上同じものです。
UMLでユースケースとはアクターによるシステム利用の方法(方法は英語でmethod)を表しています。
"システムがアクターに対して提供するサービス"や"アクターが起動するシステムの振る舞い"とも言えます。

ユースケース、方法(method)、サービス、interactorsは同じ意味なんです。
interactorsをよく見たらinter-とactorsの合成語に見えませんか?
inter-は接頭語で「…の間; 相互の」の意味です。
アクター(ユーザー)とアクター(システム)の間という意味になります。
UMLでアクターとアクターの間にはユースケース、つまりメソッドが存在します。

Hanamiは、ユーザーのアクションをDDD(Domain Driven Design)のサービスとして扱っています。
本当に素晴らしいフレームワークではないでしょうか?
Hanamiを使えば、BDDとDDDを使う理想的な開発が可能になります。
ますますHanamiが気に入りました。

参考

Hanamiの公式サイト

その他


  1. full-featured Ruby frameworkとは、Ruby on Railsのように開発からサービスまで全ての機能を提供するフレームワークを言います。私が知っている限り、full-featured Ruby frameworkはHanamiとRuby on Railsしかありません。 

  2. Content Security Policy(CSP)とは、クロスサイトスクリプティング (XSS) やデータを差し込む攻撃などといった、特定の種類の攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤーです。(MDNより) 

  3. X-Frame-Options HTTP レスポンスヘッダは、ブラウザがページを <frame> または <iframe> の内部に表示することを許可するかを示すことができます。サイトはこのレスポンスヘッダを、クリックジャッキング攻撃を防止するために使用することができます。これは、自分のサイトのコンテンツが他のサイトに埋め込まれないと保証することによります。(MDNより) 

  4. ユーザーの入力値などを自動でescapeしてくれることだと思いますが、今頃のフレームワークでは当然なことです。 

  5. 明確な責任を持つ単純なオブジェクトは典型的なコード(ボイラープレートコード)でもっと効果的です。getterやsetterメソッドなどがこれにあたります。 

  6. 私がHanamiが好きな理由はまさしくこれです。 

  7. クリーンアーキテクチャについて和訳されたものがありました。http://blog.tai2.net/the_clean_architecture.html 

  8. データベースに保存された書籍のデータを表示することではなくハードコーディングとして書いた行為 

  9. Railsでよく使われたdecent_exposureというgemがあります。コントローラーの中で宣言的インターペースを作ってくれるヘルパーですが、多分これと同じものです。 

  10. エンティティではないアクションにバリデーションルールを定義することができます。