Hanamiで異なるappのroutesを使う場合

Hanami Guides / 1.1 / Routing / Basic Usage を見ると、Railsと同じように、
(Hanamiガイドより引用)

<%= routes.path(:greeting) %>
<%= routes.url(:greeting) %>
Or
<%= routes.greeting_path %>
<%= routes.greeting_url %>

としてroutingのパスが取得できる。

ただし、これはあくまでそのapp内でconfig/routes.rbで定義したルーティングのパスを取得だけとなる。異なるappのルーティングのパスを取得したい場合は、「app名.routes.ルーティング名_path」で指定ができる。

(例)
apps
 ├ gnote ← Gnoteアプリ
 ├ gnote_api ← GnoteApiアプリ

とあった場合に、Gnoteアプリから、GnoteApiアプリのlikesアクションのルーティングのパスを取得する場合は、Gnoteアプリ内で、「GnoteApi.routes.likes_path」と指定する。

なお、ルーティング名は、Railsと同じように、ターミナルから、「bundle exec hanami routes」を打てば表示される。その中のNameがルーティング名になる。

■参考サイト
Referencing a different app’s routes in Hanami

HanamiのRepositoryで作成した独自SQLクエリーのデータにアクセスする

Repositories: SQL Queries – Hanami に書かれているとおり、Repositoryに定義するだけだが、このままだとviewに渡されるまでメソッドが実行されないので、InteractorsとかControllerでクエリー結果のデータにアクセスしようと思っても、アクセスできない。

■HanamiのDBクエリー

HanamiのDBクエリーは、Ruby Object Mapper(ORM)Sequel が使われている。

以下、Hanami Guide v1.1からの引用。

Hanami queries are based on gems from ROM project, namely rom-repository and rom-sql. The gem rom-sql is itself based on Sequel project.
Learn more on how to craft queries with ROM and Sequel.

■最初から用意されているメソッドはアクセス可能

Repositories Overview #Interface – Hanami に書かれている、最初から用意されているallとかfirstなどのメソッドは、おそらくすぐにcallされているためか、データにアクセス可能。

■データにアクセスする方法

.call.collectionをつけると、クエリーを即時実行してデータにアクセスができる。

(例)
lib/gnote/repositories/message_repository.rb
—————————————————————–
class GnoteMessageRepository < Hanami::Repository
・・・
 def most_recent_posts(most_recent_start_date)
    gnote_messages
      .where(
        created_at: most_recent_start_date..CURRENT_DATE,
        is_deleted: nil
      )
      .order{ created_at.desc }
      .call.collection
  end
・・・
end
—————————————————————–

lib/gnote/interactors/index_messages.rb
—————————————————————–
require ‘hanami/interactor’
・・・
    def call
      @most_recent = @repository.most_recent_posts(MOST_RECENT_START_DATE) # call  lib/gnote/repositories/message_repository.rb
      Hanami.logger.debug @most_recent.inspect
    end
・・・
end
—————————————————————–

コンソールの出力結果
—————————————————————–
[tanebox] [DEBUG] [2018-01-05 23:02:48 +0700] [#2,
:created_at=>2018-01-05 07:51:54 UTC, :updated_at=>2018-01-05
07:51:54 UTC, :author_hash_ip=>nil, :content=>”カレー作ったえらい”,
:is_deleted=>nil}>, #1,
:created_at=>2018-01-04 13:35:21 UTC, :updated_at=>2018-01-04
13:35:21 UTC, :author_hash_ip=>nil, :content=>”早起きできた!”,
:is_deleted=>nil}>]
—————————————————————–

■.call.collectionをつけない場合

コンソールの出力結果(クエリー文が出力される)
—————————————————————–
[tanebox] [DEBUG] [2018-01-05 22:43:34 +0700] #= ‘2017-12-29’) AND (`created_at` <= ‘2018-01-06’) AND (`is_deleted` IS NULL)) ORDER BY `created_at` DESC”>>
—————————————————————–

■.callのみつけた場合

コンソールの出力結果(クエリー文とデータオブジェクトが出力される)
—————————————————————–
[tanebox] [DEBUG] [2018-01-05 22:53:52 +0700] #@source
=#= ‘2017-12-29’) AND (`created_at` <= ‘2018-01-06’) AND (`is_deleted` IS NULL)) ORDER BY `created_at` DESC”>>, @collection=[#2,
:created_at=>2018-01-05 07:51:54 UTC, :updated_at=>2018-01-05
07:51:54 UTC, :author_hash_ip=>nil, :content=>”カレー作ったえらい”,
:is_deleted=>nil}>, #1,
:created_at=>2018-01-04 13:35:21 UTC, :updated_at=>2018-01-04
13:35:21 UTC, :author_hash_ip=>nil, :content=>”早起きできた!”,
:is_deleted=>nil}>]>
—————————————————————–

■.callと.collectionの意味(推測)

ROMとSequelの両方のAPIドキュメントを見たがメソッドの解説を発見できなかったので以下推測。
・call → rubyのcallメソッドと同じ意味で、メソッドを即時実行(評価)する。.callをつけるまで実行されず(遅延評価)、逆にタイミングのよいときに実行させることができる。
・collection → collectionのattributeにアクセスする。.collectionをつけないと、@source(クエリー文)と@collection(データオブジェクト)が出力される。

■.callと.collectionをつけなかった場合のクエリー実行場所(推測)

Hanamiのソースコードを読んでないのであっているかわからないが、各レイヤーでdebugした結果だと以下のとおり。

ユーザーアクセス → Routes → Controller → Interactor → Repository, Entity → Interactor → Controller → |Viewレイヤーでクエリー実行| View, Template → 画面描画

■参考ソースコード

pinfluence/lib/pinfluence/repositories/moment_repository.rb

Hanamiのデバッグは Hanami.logger.debug

Hanamiでデバッグするときは、
「Hanami.logger.debug 調べたいもの.inspect」とすると、コンソールに「[web] [DEBUG] [2018-01-05 10:14:35 +0700] {:a=>1, :b=>2, :c=>3}」みたいな感じで 、中身が出力される。 (Raisだと、Rails.logger.debugっていうやつ)

(例)Hanami.logger.debug @post.inspect

■参考情報
Each project has a global logger available at Hanami.logger that can be used like this: Hanami.logger.debug “Hello”
Hanami – Logging

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
———————————————————————————-