Hanamiでcustom helper

Custom Helpers – Hanami を参考に独自のヘルパーを作成したが、「NameError: uninitialized constant Helpers」が表示されてハマった。
原因は、Custom Helpers Guide – load_paths order issue #112 で書かれている通り、apps/web/application.rb の load_paths に記述する時に、helpersを最初に記述しておかないとダメだった。自分の場合はhelpersの記述を一番最後にしていたのでエラーが表示されていた。ちなみに同ファイルの「view.prepare」の記述については、特に順番は関係ない。

■設定のメモ

apps/web/application.rb
———————————————–
      view.prepare do
        include Hanami::Helpers # Hanamiのヘルパーモジュール読み込み
        include Web::Assets::Helpers # app/web/assets/helpersを作成して利用想定?
        include Web::Helpers::FormatDate # 今回カスタムヘルパーとして新しく記述した
      end
———————————————–

■注意事項

load_pathsとかの設定をしたら、Cmd-C、bundle exec hanami sで再起動すること。設定系なので記述したものがうまく反映されない場合があった。

■その他メモ

・appごとにヘルパー作成するのもよし、コードを共通化する場所に置いて使うかもよし。「In our settings (apps/web/application.rb), there is a code block that allows to share the code for all the views of our application.」 – Share Code – Hanami
・helpersを設置している方のコード。この方はload_pathsは使わずにヘルパー使いたい場所でincludeして読み込んでいる。 nfilzi/housing-list
・ヘルパーは全然関係ないけど、、.、Hanamiのアーキテクチャーの考え方とDry Rubyの実装方法 hanami-architecture – hanami

Hanami RubyのCSP対応

HanamiMaterialize Google Fonts をCDNで読み込んだらChromeのコンソールで以下のような警告メッセージが出た。

「Refused to load the font ‘https://fonts.gstatic.com/ea/roundedmplus1c/v1/RoundedMplus1c-Thin.woff2’ because it violates the following Content Security Policy directive: “font-src ‘self'”. 」

Contents Security Policy(CSP)というXSSみたいな攻撃を防ぐためのセキュリティを強化するためのものらしい。あらかじめ許可していないURLが呼び出されないようにするもの。Hanamiはセキュリティも堅牢という方針(?)のせいか、これがデフォルトで効いていて、許可していないjsやらfontやらを外部CDNから読み込もうとすると警告が出る。

■対応方法

1.apps/web/application.rb に許可したいURLを以下を追記する(警告が出たものを追記する) 

————————————————————
      security.content_security_policy %{
        form-action ‘self’;
        frame-ancestors ‘self’;
        base-uri ‘self’;
        default-src ‘none’;
        script-src ‘self’;
        connect-src ‘self’;
        img-src ‘self’ https: data:;
        style-src ‘self’ ‘unsafe-inline’ https:;
        font-src ‘self’ https://fonts.gstatic.com https://cdnjs.cloudflare.com;
        object-src ‘none’;
        plugin-types application/pdf;
        child-src ‘self’;
        frame-src ‘self’;
        media-src ‘self’
      }
————————————————————
●’self’は自分のURLを指す。http://localhost:2300/で動かしていたら、それが指定されるっぽいです
●「*」を指定すれば全て許可になるようですが、Hanamiが強化してくれたセキュリティが台無しになってしまうので、それは使わない方が良さそうです
●jqueryとかmaterialize.min.jsもCDN経由にしたい場合は、script-srcに指定すればたぶんOK
【注意】 ①URLはシングルクォーテーションで囲まない ②複数指定の場合は空白スペース

2.Hanamiを再起動(重要!) 

————————————————————
$ Ctrl+C
$ bundle exec hanami s
————————————————————
【注意】 ローカルで動かしているWEBrickだと再起動しないとCSP設定が読み込まれない

3.ChromeのConsoleでエラーが表示されていないことを確認する

■補足(Google Fonts対応で記述が必要なソース)

apps/web/templates/application.html.erb
————————————————————


————————————————————

public/assets/common/stylesheets/style.css
————————————————————
body { font-family: “Rounded Mplus 1c”; }
———————————————————— 


■参考情報

Hanami – Content-Security-Policy
Hanami – Assets Content Delivery Network (CDN)

RubyのHanamiでInteractorを使う

シンプルでスケールしやすく、軽量で早い、そしてDDD(Domain Driven Development)思想を取り入れているフレームワークのHanamiでInteractorを使う方法についてのメモです。

■HanamiのGetting Started

HanamiのGetting StartedはInteractorを使うところがなく、Controllerにビジネスロジックを書いていますが、単一責任の考えより、HanamiではService層にあたるInteractorにロジックを切り出して書くことを推奨しています。

Hanami v1.1 Getting Started
※他の方が日本語で説明されているページもありますが本筋と関係ないので省略

■HanamiのInteractor

Getting Startedでは、Interactorの実装まで書かれておらず、個別のページで説明されています。これはまだ日本語版はないようです。内容はGetting Startedの続きのような形で書かれています。

Hanami v1.1 Architecture – Interactors

■Interactorの実装

上記をそのまま実装するだけですが、Controller側の実装は書かれていなかったため(テストコードはありましたが)、以下にController含めた、ポイントになる箇所のソースコードを記載します。

[lib/bookshelf/interactors/add_book.rb]
———————————————————————————-
require ‘hanami/interactor’

class AddBook
  include Hanami::Interactor

  expose :book

  def initialize(repository: BookRepository.new, mailer: Mailers::BookAddNotification.new)
    @repository = repository
    @mailer = mailer
  end

  def call(title:, author:)
    @book = @repository.create({title: title, author: author})
    @mailer.deliver
  end
end
———————————————————————————-
↑ Dependency Injection(依存性の注入)の考えより、initializeとcallで処理を分けています。これを分けずにcallに全部書くことも可能です。その場合は、initializeにparamsを引数にしてexposeし(@params = params)、callは何も引数を取らずリポジトリもcall内で取得、という書き方になります。

[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 = AddBook.new.call(params[:book])

        redirect_to “/books”
      else
        self.status = 422
      end
    end
  end
end
———————————————————————————

↑ 念のため補足しますと、HanamiのInteractor説明ページのテストコード(Specのコード)をControllerにほとんど転記しているだけです。

以下引用:Hanami Interactor – Creating Book

Edit spec/bookshelf/interactors/add_book_spec.rb:
require 'spec_helper'

describe AddBook do
  let(:interactor) { AddBook.new }
  let(:attributes) { Hash[author: "James Baldwin", title: "The Fire Next Time"] }

  describe "good input" do
    let(:result) { interactor.call(attributes) }

    it "succeeds" do
      expect(result).to be_a_success
    end

    it "creates a Book with correct title and author" do
      expect(result.book.title).to eq("The Fire Next Time")
      expect(result.book.author).to eq("James Baldwin")
    end
  end
end

■エラーメッセージの例

・ArgumentError: unknown keyword: book
→ newにparamsを入れていませんか?(例)AddBook.new(params).call
newではなくcallに引数を入れます。

・ArgumentError: unknown keyword: title, author
→ newにparams[:book]を入れていませんか?(例)AddBook.new(params[:book]).call
newではなくcallに引数を入れます。

■参考になるサイト

以下はInteractorのcallではなくinitializeにparamsを引数にした書き方になっています。

Rubyist Magazine – HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか
→ Interactorの説明が非常に詳しく、分かりやすく書かれています

The Pragmatic Hanami by kbaba1001
→ 上記と同じ方が書かれていますが、こちらも分かりやすいです

ossboard – lib/interactorossboard – apps/controller
→ Hanamiで作っているossboardというプログラムがGitHubに載っているのでInteractorの実装方法を見ることができます

■その他

Hanami Bookshelf (example application) 
→ ちなみにHanamiのGetting StartedのソースはGitHubにあります

■(オマケ)initializeにparamsを引数にする場合のInteractorの書き方

[lib/bookshelf/interactors/add_book.rb]
———————————————————————————-
require ‘hanami/interactor’

class AddBook
  include Hanami::Interactor

  expose :book_attributes

  def initialize(params)
    @params = params
  end

  def call
    @book_attributes = BookRepository.new.create(@params[:book])
  end
end
———————————————————————————- 

[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]) #コメントアウト
        result = AddBook.new(params).call
        @book = result.book_attributes #.book_attributesを忘れずに

        redirect_to routes.books_path
      else
        self.status = 422
      end
    end
  end
end
———————————————————————————-