Hanamiを始めよう! – Hanamiのチュートリアル –

Hanamiのガイドページへようこそ!

HanamiはRubyのマイクロサービスWEBフレームワークです。シンプルさや安定したAPI、最小のDSLを特徴としています。 もしあなたが保守性が高く、安全で、より速く、テストが容易なWebアプリケーションを構築するための新しい方法を探しているのであれば、Hanamiはそれにぴったりです。

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

しかし、皆様に気に留めてもらいたいことがあります。もしあなたがプログラミングの全くの初心者である場合でも、豊富な経験を保つ場合でもこのガイドに掲載してある学習プロセスは難しい可能性があります。
10年の経験を経て、アーキテクチャを組む方法を学んできた場合に、それらを覆すことは痛みを伴うかもしれません。しかし、挑戦をしなければ何かが変わるということはありません。

ただし、時にHanamiの挙動が正しくないように見えるかもしれませんが、それはそれはあなたのせいでない場合があります。Hanamiは発展途中で設計上の問題や、あるいはバグの可能性があります。私はコミュニティとともに日々より良いHanamiを作るための最善の努力をしています。

このガイドでは、最初のHanamiプロジェクトを設定し、シンプルな本棚のWebアプリケーションを構築します。 このガイドにおいて、私たちはHanamiフレームワークの主要なすべてのコンポーネント、テスト手法について触れます。

もしもあなたがHanamiについて聞く人が周りにいなかったり、コーディングがわからずにまたはイライラした場合は私達のチャットにヘルプを求めてください。

あなたとお話するのを楽しみにしています。

それではHanamiをお楽しみください!

Luca Guidi – Hanamiの作成者  –



Hanamiを始める前に

Hanamiを始める前に幾つかの条件を確認してみましょう。まず、Webアプリケーションの開発の基本についてです。。

Hanamiは主にコンソール上で作業をし、BundlerRakeを使用してアプリケーションを作成します。また、あなたはモデル・ビュー・コントローラーのMVCフレームワークに精通している必要があります。また、このガイドでは私たちはSQLiteのデータベースを使用します。もし同じ環境でHanamiのガイドを勧めたいなら、始める前にお使いのシステムでRuby 2.3以降のバージョンがインストールされているか、SQLiteの3以降のバージョンがインストールされていることを確認してください。


新しいHanami プロジェクトの作成

新しいHanamiプロジェクトを作成するためには、RubygemsからHanamiのgemをインストールする必要があります。インストールするとコンソール上でhanamiコマンドを使うことができるようになります。
newのコマンドで新しいhanamiプロジェクトのディレクトリが作成されます

% gem install hanami --pre
% hanami new bookshelf

デフォルトでは、プロジェクトはSQLiteデータベースを使用するように設定されますが、以下のように–databaseのオプションを使うことによって他のエンジンを指定することもできます。

% hanami new bookshelf --database=postgres

まずはプロジェクトに何が含まれるのか見てみましょう。

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

6 directories, 3 files

ここで知っておくべきディレクトリの詳細はこちらです。

  • Gemfile :Rubygemsの依存関係を定義します。
  • Rakefile :Rakeタスクが書かれているファイルです。
  • apps :Rackと互換性のある一個以上のWebアプリケーションが含まれています。ここでは最初に生成された生成されたWeb :アプリケーションも含まれています。それらにはコントローラ、ビュー、ルートやテンプレートが含まれます。
  • config :コンフィギュレーション・ファイルです
  • config.ru :Rackサーバの設定です
  • db :データベーススキーマとマイグレーションが含まれています。
  • lib :ビジネスロジックとエンティティとリポジトリを含むドメインモデルが格納されています。
  • public :コンパイルされた静的なアセットが含まれています。
  • spec :テストが含まれています。

以下のコマンドでは、Bundlerを使ったGemのインストールをし、サーバーの起動をすることが出来ます。

% bundle install
% bundle exec hanami server

すると…

おめでとうございます!
あなたの最初のHanamiプロジェクトが作成されました。http:// localhost:2300を訪れてみましょう。するとこのような画面が表示されるはずです。

screen-shot-2017-02-19-at-23-32-53


Hanamiのアーキテクチャ

Hanamiのアーキテクチャは同じRubyプロセス内でHanami(及びRack)のアプリケーションを複数ホストすることが出来ます。

これらのアプリケーションは、apps/の以下におり、ユーザーフェイシングのウェブインタフェースやアドミニストレータのページ、メトリックス、HTTP APIなど、様々な役割のコンポーネントになりえます。

すべてのこれらの部品は、lib/以下に存在するビジネスロジックを届けるためのメカニズムです。
lib/はのモデルが定義されている場所で、サービスが提供する機能を構成するため、それぞれのモデルが相互に対話する場所です。

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


テストの書き方

私達のアプリを起動して、アクセスしたときに初めて見るオープニング画面は、定義されたルートが存在しないときに表示されるデフォルトページです。

HanamiはBDD(Behavior Driven Deployment:ビヘイビア駆動開発)の手法を用いて開発することが奨励されています。そして、最初のカスタムページを表示するために、まずは高次元の機能テストのコードを書いていきます。

# 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はビヘイビア駆動開発(BDD)のワークフローを実現する環境としての準備が整っていますが、決して特定のテストフレームワークに縛られるという意味ではありません。また、Hanamiには特別なインテグレーションやライブラリが付いているわけではありません。

それではデフォルトで設定されているMinitestを見て見ましょう。しかしもちろんRSpecも使うことができます。rspecで開発進める際にはプロジェクトを作成時に--test=rspecオプションをつけましょう。それによってHanamiはRSpecのためのヘルパーとstubファイルを生成します。


リクエストについて

先ほどテストを作りました。以下のコマンドでそのテストが失敗したか確認することができます。

% 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アクションを指定します。
ルーティングについてはルーティングガイドを参照してください。
これでルート設計はできたので、続いてその実態のアクションを作って行きましょう。

# 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

こちらの定義されたクラスは空で、テンプレートをレンダリングする以外のことは何もしません。これは先ほど作成したテストをパスするために必要な記述です。
次にやらなければならないことは、本棚の見出しを表示するページを作ることです。

# 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のコンポーネントに追加するための新しい方法を試して見ましょう。
本棚プロジェクトの目的は本を管理することです。

私たちは私達のデータベースに本をためていき、ユーザーが本棚プロジェクトで本を管理できます。
最初のステップは、すべての書籍のリストを表示する機能をシステムに追加することです。

次にその新しい機能のテストを書いて行きましょう!

# 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 Generators

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>
by <strong>Martin Fowler</strong></div>
<div class="book">
<h3>Test Driven Development</h3>
by <strong>Kent Beck</strong></div>
</div>

入力し終えたら変更を保存し、テストをパスするか確認してください。

続いて、コントローラとアクションについて触れます。コントローラとアクションは似ていて混乱するかもしれません。
アクションはHanamiアプリケーションの基礎を形成するのに対し、コントローラは単なるアクションの記述されたモジュールグループです。
コントローラは概念的には現在作成している本棚プロジェクトに存在しますが、このプラクティスではアクションだけを扱います。

私たちは、この練習アプリケーション作成において、新しいエンドポイントを作成するためにジェネレータを使用しました。懸命なあなたは作成したテンプレートがデフォルトのままになっていることに気づいているかもしれません。
それでは直していきましょう。


レイアウト

ヘッダーやフッター、ナビゲーションバーなど、全てのテンプレートに同じことを書く繰り返しの作業を避けるために、共有レイアウトを使用することができます。
それでは対象のファイルを開き、共有レイアウトを試して見ましょう。
apps/web/templates/application.html.erb

<!DOCTYPE HTML>
<html>
  <head>
    <title>Bookshelf</title>
  </head>
  <body>
<h1>Bookshelf</h1>
<%= yield %>
  </body>
</html>

上記のようにすることで、あなたは他のテンプレートから重複行を削除することができます。

レイアウトは、ベーステンプレートのようなものですが、通常のテンプレートをラップするために使用されます。yieldラインは、通常のテンプレートの内容に置き換えられます。
それは私たちが繰り返しヘッダーとフッターを書くのを避けるのに最適な方法です。


エンティティとデータのモデル化

Bookのデータをハードコーディングすることは基本的にはあってはならないこととされています。
今度は作成中のアプリケーションがもっとダイナミックになるように動的なデータを追加して見ましょう。

このプログラムでは、データベースでbookを保存し、ページ上に表示します。
そうするためには、データベースの読み書きができる必要があります。そのために、エンティティとリポジトリを追記しましょう。

  • エンティティは、ドメインオブジェクトです(例えばBookです)一意の値で識別されます。
  • リポジトリは、エンティティと永続化層の間をとりもちます。

エンティティはデータベースに関して全く気にしません。これがHanamiを軽量テストしやすいフレームワークに保ちます。

このような理由から、Bookの依存するデータを永続化するためにリポジトリが必要とされます。
エンティティとリポジトリについてもっと詳しくなるのであれば、モデルガイドを参照してください。

HanamiはモデルのためにGeneratorを提供しています。さて、次はBookのエンティティとそれに対応するレポジトリを作って見ましょう。

% bundle exec hanami generate migration create_books
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

上記を見るとわかるようにGeneratorはエンティティ、リポジトリおよび添付のテストファイルを提供します。


データベーススキーマを変更して移行するには

さて、続いて作成されたtitleauthorのフィールドを含む作成されたマイグレーションファイルを編集しましょう。

# 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


エンティティでの作業

エンティティはプレーンなRubyオブジェクトにとても近いものです。
私たちはプレーンなオブジェクトの状態をいかに保つかということに焦点を当てなければいけません。

まずは単純なエンティティクラスを作成する必要があります。

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

このクラスでは、パラメータを初期化するために渡す各属性のゲッター及びセッターを生成します。
ユニットテストにおいて、それらが予想通り動くかを確認することができます。

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

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


リポジトリの使用

これでリポジトリを使用して遊んでみる準備が整いました。Hanamiはconsoleコマンドを持ち、Hanamiアプリケーションが事前にロードされた状態のIRbを起動することができます。
すなわち、あなたはコンソールでHanamiのオブジェクトを使うことができるのです。

% bundle exec hanami console
>> repository = BookRepository.new
=> => #<BookRepository:0x007f9ab61fbb40 ...>
>> repository.all
=> []
>> book = repository.create(title: 'TDD', author: 'Kent Beck')
=> #gt;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)
=> #gt;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のリポジトリは、データベースから1つ以上のエンティティをロードするためのメソッドを持っています。そして、レコードの作成と更新をすることができます。
また、リポジトリはカスタムクエリを実装するために新しいメソッドを定義する場所です。

今までの作業で、私たちはHanamiがデータをモデル化するための、エンティティおよびリポジトリを使用する方法を学んできました。エンティティは、レポジトリがエンティティをデータストアに変換するためのマッピングを使うあいだの動きを表しています。
また、私たちはデータベーススキーマへの変更適用のためにマイグレーションを使用することができます。


動的データの表示

Hanamiの新しい経験モデリングデータを使うと、私たちはBookのリストページに動的なデータを表示することができるようになります。それでは以前に作成した機能のテストを修正してみましょう。

# 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

ここでは、テストに必要なレコードを作り、正常な数の本のクラスがページに表示されることを確認します。
テストを実行すると、データベースコネクションに関するエラーと思われるエラーが表示されるでしょう。
ここで思い出して欲しいのは、私たちはデベロップメントの環境でデータベースの設定をしただけで、テスト環境のデータベースは設定していないということです。

% HANAMI_ENV=test bundle exec hanami db prepare

さて、これで環境が整ったので、以前記入した静的なHTMLをテンプレートから取り除くことができるようになります。
Viewはレンダリングされる過程で、全ての使用可能なレコードをループして表示する必要があります。
、それらをレンダリングします。私たちの見解では、この変更を強制的にテストを書いてみましょう:

# 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('

There are no books yet.

')
    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('

There are no books yet.

')
    end
  end
end

表示するbookがなかった時は、シンプルなメッセージをインデックスページで見せることができます。
ビューをデータとともにレンダリングする方法は比較的簡単です。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>
<%= book.author %></div>
<% end %></div>
<% else %>

There are no books yet.

<% end %>

もしテストを今すぐ走らせるとしたら、テストはパスしないでしょう。
なぜならまだコントローラのアクションは実際にBookをViewに対して公開(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

コントローラのアクションに対してテストを書くことは基本的に作業が二倍になることを意味し、
あなたはラックと互換性のあり、ヘッダーやコンテンツやステータスを格納しているレスポンスオブジェクトの配列、もしくはコールした後にコンテンツを後悔する仕組みをもつアクション自体に対するテストを書くことになります。
それでは、
アクションが:booksを公開するように実装して行きましょう。

# 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はそれをビューに渡すことができるようになります。
それではmもう一回テストを通して見ましょう。次のテストはパスするはずです。

% 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


レコードを作成するフォームの構築

最後のステップのうちの一つは、システムに新しい本を追加することができるようになることです。
流れは単純です。まず本の詳細を入力するためのフォームでページを作成します。

ユーザーがフォームを送信すると、新しいエンティティが構築・保存され、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'

新しいテンプレートに関しては少し興味深く感じていただけるかもしれません。
それは、私たちはHanamiのフォームビルダーをBookエンティティのHTMLフォームを構築するために使うからです。


フォームヘルパーの使用

それではフォームヘルパーを使用して見ましょう。

フォームヘルパーを使用してapps/web/templates/books/new.html.erbのページを編集します。

# 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 %>

これで、フォームフィールドにはタグと、

でそれぞれのフィールドをラップしたコンテナが、HanamiのHTMLビルダーヘルパーを使用して追加されました。

 

フォームを送信

フォームを送信するためにはまだ別のアクションが必要です。それではBooks::Createアクションを作成してみましょう

% bundle exec hanami generate action web books#create --method=post

次にアプリに新しいルートを追加します。

# apps/web/config/routes.rb
post '/books', to: 'books#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' }] }

  before do
    BookRepository.new.clear
  end

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

    action.book.id.wont_be_nil
    action.book.title.must_equal params[: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

これらのテストに合格するように実装をするのはとても簡単です。
私たちはすでにデータベースにエンティティを書き込むことができ、そしてどのように使用できるかについて見てきました。 またredirect_toメソッドを使えば、ページへリダイレクトすることができます。

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

    expose :book

    def call(params)
      @book = 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

テストが成功しましたね!おめでとうございます!


フォームの検証でセキュリティ確保

普段、私たちは堅牢なフォームを構築するためにいくつかの追加の対策を必要とします。例えば、ユーザーが任意の値を入力せずにフォームを送信した場合にどうなるかを想像してみてください。

私たちは、不正データや、整合性に違反しているデータでデータベースを埋めることができます。
それらの対策として不正なデータからシステムを守る必要があります!

テストにおいて検証を表現するためには以下のことを考える必要があります。

もしも検証に失敗したら?
一つの選択肢としては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 }

  after do
    BookRepository.new.clear
  end

  describe 'with valid params' do
    let(:params) { Hash[book: { title: '1984', author: 'George Orwell' }] }

    it 'creates a new book' do
      action.call(params)
      action.book.id.wont_be_nil
    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 're-renders the books#new view' do
      response = action.call(params)
      response[0].must_equal 422
    end

    it 'sets errors attribute accordingly' do
      response = action.call(params)
      response[0].must_equal 422

      action.params.errors[:book][:title].must_equal  ['is missing']
      action.params.errors[:book][:author].must_equal ['is missing']
    end
  end
end

このテストは2つのシナリオを定義しています。
一つ目が私たちにとって一番幸せなパス、つまり私たちの思惑通りにユーザーがフォームを入力した場合、そして2つ目が検証に失敗した場合です。記載したテストがパスするように検証のプログラムを実装しましょう。

あなたはエンティティに検証のルールを記載することができますが、Hanamiはまたインプットのソースコードにできるだけ近い位置、例えばアクションに検証のルールを定義することもできます。

Hanamiコントローラのアクションはparamsクラスを、許容できるパラメータか判断するために使用することができます。

このアプローチは何のパラメータが使われているかのホワイトリスト(信頼できないユーザーにより、mass assigmentで余分なパラメータを割り振られることを防ぐために許容されない他のパラメータは破棄されます) 及び、どのような値が許容されるかの判断に使うことができます。

このケースではネストしてあるBookのtitle及びauthorがパラメータに含まれるべきであるということになります。

この検証では、リクエストに含まれるparamsが無効である場合には、エンティティの作成およびリダイレクトを制限することができます。

# 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(処理不可能なエンティティ)のように設定されています。その後コントロールは対応するビューにリクエストをパスします。この時ビューはレンダリングするテンプレートを知っている必要があります。
この場合に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の検証をするのには十分にスマートだからです。ここではパラメータで見つかった値とフォームフィールドの値を付き合わせ、フォームフィールドの値を埋めてくれる役割も担います。ユーザーが一つの値のみを入力して、不完全な状態でフォームを送信した場合、レンダリングされた画面にサブミットした時のinputの値が表示されるため、ユーザーが再度値を入力する手間を無くします。

それではもう一度テストを実行し、それらがすべてパスしているか確認してください!


検証時のエラーの表示

何かの問題が起こった場合に、ユーザーフォームを見せるだけるよりは、私たちは何が期待されているのかのヒントをユーザーに見せるべきです。さて、それでは不正が起きた時の通知を見せる機能を試して見ましょう。

まず、paramsにエラーが含まれている場合は、ページに含まれるエラーのリストが見えることが期待されます。

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

class NewBookParams < Hanami::Action::Params
  params do
    required(:book).schema do
      required(:title).filled(:str?)
      required(:author).filled(:str?)
    end
  end
end

describe Web::Views::Books::New do
  let(:params)    { NewBookParams.new(book: {}) }
  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
    params.valid? # trigger validations

    rendered.must_include('There was a problem with your submission')
    rendered.must_include('Title is missing')
    rendered.must_include('Author is missing')
  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” アプリケーションのルートファイルを開いて見ましょう。

# 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スタイルのルートを構築するために便利なヘルパーメソッドを提供します。これらを使うことによってルーターの扱いを簡素化することができます。

resources :books, only: [:index, :new, :create]
root to: 'home#index'

なんのルートが定義されているのかの感覚をつかむために、特別なroutesというコマンドラインタスクを使用することができます。これによって、ルーティングが最終的にどのように行われるかを確認することができます。

% bundle exec hanami routes
                Name Method     Path                           Action

               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
                root GET, HEAD  /                              Web::Controllers::Home::Index

hanami routesの出力は定義されたヘルパーメソッドの名前を表示します(サフィックスの_path_urlをつけたものをroutesヘルパーで呼ぶことができます)。
また、許可されたHTTPメソッド、パスと最終的にリクエストをハンドルするコントローラーアクションも表示されます。

今、resourcesヘルパーメソッドを適用し、名前がつけられたルートメソッドを使用することができるようになりました。今までどのようにform_forを使用してフォームを構築したか覚えていますか?

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

定義したルータが、すでにでにフォームでどのルートを指すようにすればいいのかわかっているのに、テンプレートにハードコードパスを含めることは愚かな選択です。
私たちはroutesヘルパーメソッドにビューとアクションからアクセスすることができます。

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

こちらのファイルにも同様の変更を行うことができますapps/web/controllers/books/create.rb

redirect_to routes.books_path


章の終わりに

これであなたの最初のHanamiプロジェクトは完了です。おめでとうございます!

最後に私たちが何をやったか確認してみましょう。
私たちは、Hanamiのメジャーなフレームワークがリクエストをトレイスして、どのようにそれぞれの仕組みが関係するかを理解してきました。そして、エンティティとレポジトリを使ってどのようにモデル化できるかを見てきました、また、フォームの構築の仕方やデータベースのスキーマやインプット検証のソリューションにも触れました。

私たちは長い道のりを歩んできましたが、探索はここで終わりではありません。もっとたくさんのことをガイドでは発見することができます。
ぜひこれから続く他のガイドや、HanamiのAPIドキュメントを、読んで理解を深めてください。
また、ソースコードを見て見てください。

素晴らしいアプリケーションの構築をお楽しみください!