はじまる前に
Hanamiはとても素晴らしいフレームワークです。
最近1.1.0にアップデートされました。
この記事はHanami公式サイトのIntroductionを和訳したものです。
辞書を引きながら書いてみましたが、間違いや足りないところが多いと思います。
ご指摘いただけると幸いです。
紹介
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プロジェクトの栄光を満喫してください!
このような画面が表示されるはずです。
Hanamiのアーキテクチャ
Hanamiのアーキテクチャは、複数のHanami(およびRack)アプリケーションを同じRubyプロセスの中でホストできます。
これらのアプリケーションは、apps/の配下にあります。
それらは、我々の製品になることができます。
例えば、ユーザー向けのWebインターフェイス、管理ペイン、メトリック、HTTP APIなど。
これらのパーツすべては、libs/の配下にあるビジネスロジックのための配信メカニズムです。
これは、モデルが定義されている場所であり、我々の製品が提供する機能を構成するために相互作用します。
Hanamiのアーキテクチャは、クリーンアーキテクチャに大きな影響を受けています。7
最初のテストを書く。
私たちがアプリを見ているときに見えるオープニング画面は、定義されたルートがないときに表示されるデフォルトのページです。
Hanamiは、Webアプリケーションを作成する方法としてビヘイビア駆動開発(BDD)を推奨しています。
最初のカスタムページを表示するために、高度な機能テストを作成します。
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
さあ、それを通らさせましょう。このテストをパスするために必要なコードを追加します。段階的に。
最初に追加する必要があるのはルートです。
root to: 'home#index'
homeコントローラーのindexアクションにアプリケーションのルートURLを指し示します。
詳細については、ルーティングガイドを参照してください。
これでindexアクションを作成できます。
module Web::Controllers::Home
class Index
include Web::Action
def call(params)
end
end
end
これは、ビジネスロジックを実装していない空のアクションです。
各アクションには対応するビューがあります。
そして、これはRubyオブジェクトであり、リクエストを完了するために追加される必要があります。
module Web::Views::Home
class Index
include Web::View
end
end
...それは空であり、テンプレートをレンダリングする以外に何もしません。
これは、テストをパスするために修正する必要のあるファイルです。
私たちがする必要があるのは、bookshelfという見出しを追加することだけです。
<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プロジェクトの目的は書籍を管理することです。
私たちはデータベースに書籍を保存します。
そして、ユーザーが我々のプロジェクトでそれらを管理するようにします。
最初のステップは、システム内のすべての書籍のリストを表示することです。
私たちが達成したいことを記述する新しい機能テストを書いてみましょう。
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が生成されます。
ジェネレータは我々に空のアクション、ビュー、テンプレートを提供します。
また、デフォルトルートを次の場所に追加します。
get '/books', to: 'books#index'
あなたがZSHを使用している場合、あなたは次のようなメッセージを見るかもしれません。
zsh: no matches found: books#index
その場合は、次のものを使えます。
% hanami generate action web books/index
我々のテストをパスするために、新しく生成されたテンプレートファイルを修正する必要があります。
<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を開き、次のように修正します。
<!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を含めているからです。)
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オブジェクトに本当に近いものです。
私たちはそれから欲しい振る舞いとその時になって始めてそれをどう保存するべきなのかに焦点を当てるべきです。
今のところ、単純なエンティティクラスを作成する必要があります。
class Book < Hanami::Entity
end
このクラスは、paramsを初期化するために渡す各属性ごとにgetterとsetterを生成します。
単体テストですべてが正常に動作することを確認できます。
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がエンティティとリポジトリを使用してデータをモデル化する方法を見てきました。
エンティティは行動を表します。もう一方、リポジトリはデータストアへエンティティを交換するためにマッピングを使います。
私たちはデータベーススキーマに変更を適用するマイグレーションを使えます。
動的データの表示
新しい経験のデータモデリングを使用して、書籍リストページで動的データを表示する作業を行うことができます。
先ほど作成した機能テストを調整しましょう。
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を削除することができます。
私たちのビューは、利用可能なすべてのレコードをループしてレンダリングする必要があります。
この変更を強制するためにテストを作成しましょう。
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は、独立したテストが容易な最小限のインターフェースを備えたシンプルなオブジェクトで設計されていますが、依然として大きな仕事をしています。
これらの要件を実装するためにテンプレートを書き直してみましょう。
<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してないからです。
その変更に関するテストを書けます。
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するように指定しました。
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リストに戻します。
テストで表現されたこの話は次のとおりです。
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
これでアプリに新しいルートが追加されます。
get '/books/new', to: 'books#new'
興味深いちょっとした部分が新しいテンプレートになります。
何故ならBookエンティティの周りにHTMLフォームを構築するためにHanamiのフォームビルダを使うようとしているからです。
フォームヘルパーの使用
フォームヘルパーを使ってこのフォームを作成しましょう。
<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
これでアプリに新しいルートが追加されます。
post '/books', to: 'books#create'
Createアクションを実装
私たちのbooks#createアクションは2つの事をする必要があります。
単体テストとしてそれらを表現しましょう。
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を使えます。
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フォームを再レンダリングすることであり、ユーザに正しく記入するために別のショットを与えることができます。
単体テストとしてこの動作を指定しましょう。
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を、入力パラメータが有効な場合に限定することができます。
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は、フォームを再度レンダリングするために使用されます。
module Web::Views::Books
class Create
include Web::View
template 'books/new'
end
end
このアプローチはうまく機能します。
Hanamiのフォームビルダは、このアクションでparamsを検査し、フォームフィールドにparamsで見つかった値を設定できるほどスマートです。
ユーザーが送信する前に1つのフィールドのみを入力すると、元の入力が表示され、再度入力する必要がありません。
テストをもう一度やり直してみてください。
バリデーションエラーの表示
何かが間違ってしまったときに、ユーザーの鼻の前にフォームを押し込むのではなく、それらに期待されることのヒントを与えるべきです。
フォームを変更して、無効なフィールドに関する通知を表示しましょう。
まず、paramsがエラーを持っている場合、エラーのリストがページに表示されることを期待します。
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(もしあれば)をループしてフレンドリーなメッセージを表示することができます。
<% 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ファイルを開きます。
post '/books', to: 'books#create'
get '/books/new', to: 'books#new'
get '/books', to: 'books#index'
root to: 'home#index'
Hanamiは、これらのRESTスタイルのルートを構築するための便利なヘルパーメソッドを提供しています。
ルーターを少し簡略化するために使用できます。
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の中で同様な変更を加えることができます。
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の公式サイト
- http://hanamirb.org/
- http://hanamirb.org/guides/1.1/
- http://hanamirb.org/guides/1.1/getting-started/
その他
- クリーンアーキテクチャの和訳
- ユースケース図を学ぼう!
- Railsコードを改善する7つの素敵なGem - interactors
- https://github.com/collectiveidea/interactor
-
full-featured Ruby frameworkとは、Ruby on Railsのように開発からサービスまで全ての機能を提供するフレームワークを言います。私が知っている限り、full-featured Ruby frameworkはHanamiとRuby on Railsしかありません。 ↩
-
Content Security Policy(CSP)とは、クロスサイトスクリプティング (XSS) やデータを差し込む攻撃などといった、特定の種類の攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤーです。(MDNより) ↩
-
X-Frame-Options HTTP レスポンスヘッダは、ブラウザがページを <frame> または <iframe> の内部に表示することを許可するかを示すことができます。サイトはこのレスポンスヘッダを、クリックジャッキング攻撃を防止するために使用することができます。これは、自分のサイトのコンテンツが他のサイトに埋め込まれないと保証することによります。(MDNより) ↩
-
ユーザーの入力値などを自動でescapeしてくれることだと思いますが、今頃のフレームワークでは当然なことです。 ↩
-
明確な責任を持つ単純なオブジェクトは典型的なコード(ボイラープレートコード)でもっと効果的です。getterやsetterメソッドなどがこれにあたります。 ↩
-
私がHanamiが好きな理由はまさしくこれです。 ↩
-
クリーンアーキテクチャについて和訳されたものがありました。http://blog.tai2.net/the_clean_architecture.html ↩
-
データベースに保存された書籍のデータを表示することではなくハードコーディングとして書いた行為 ↩
-
Railsでよく使われたdecent_exposureというgemがあります。コントローラーの中で宣言的インターペースを作ってくれるヘルパーですが、多分これと同じものです。 ↩
-
エンティティではないアクションにバリデーションルールを定義することができます。 ↩