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

BracketsでRuby実行する拡張機能・インデント時にスペース2つ幅設定

■Bracketsとは
非常に優秀で使いやすくて高機能なSublime Textの代替できるレベルのAdobe製テキストエディター Brackets。これが無料は本当にすごい。拡張機能も豊富でなんでもできるし、デザインも素敵。
http://brackets.io/

■Brackets上でRubyプログラムを実行
拡張機能のBrackets Builderをインストールすると実行できるようになる。
(Sublime Textだと Command+B でbuild実行できるやつです)

1)ファイル – 拡張機能マネージャ – 検索窓で「Integrated Development」を検索してインストール
2)Bracketsを再起動(これ重要)
3)Command+Alt+B でRubyプログラムが実行できる(下にRun Resultが表示される)

【補足】
同じくプログラム実行できる拡張機能で「Brackets Builder」と「Brackets Builder Extended」(2つの違いはわからず)があるが、どちらも実行時の出力がちゃんとできず、文字列がおかしな表示になった。

■拡張機能のIntegrated Developmentで古いRubyのバージョンが実行される場合
設定ファイルの cmd が実行ファイルの場所を指定しているので、それを書き換えるだけ。
メニューの 編集 – Edit Builder で、実行したいRubyファイルの場所を指定する。

(例)
“cmd”: “/Users/***/.rbenv/shims/ruby $FILE”,

デフォルトだと「”cmd”: “/Users/***/.rbenv/shims/ruby $FILE”,」になっている。

Rubyでバージョンを確認するには、rubyファイル作って(.rbファイル)、「p RUBY_VERSION」を記述し、Integrated Development拡張機能をインストールした上で、Command+Alt+Bで確認できる。

【備考】
・自分の場合は、ちゃんとrbenvでインストールした$PATHを指定しているのに(しかも/usr/bin/よりも先頭に定義していて、$which rubyするとrbenvのパスが表示される)、なぜか/usr/bin/rubyのバージョンが優先実行されてしまったため、設定ファイルを編集した。
・Integrated Developmentの設定ファイルを編集する際は、jsonファイルなのでコメントアウト(#とか//とか)は使えません。直接書き換えるのがよいです。

■Bracketsでインデント幅をスペース2つ分に設定
1.タブの見え方を変更する場合
下部のステータスバーの一番右(画面から見るとつまり右下)の「タブ幅」を4から2に変更するだけ。ただし、見え方だけスペース2つ分になっているだけなので、他のエディターにコピペすると通常のタブと同じ幅になる。

2.スペース2つ分にする場合

Rubyなどでインデントする時にタブではなく、スペース2つ分の幅に設定をしたい場合は、右下の「タブ幅」をクリックすると、「スペース」に表示が切り替わるので、そこで数値を4から2に変更することでインデントがスペース2つ分の幅に変更される。

「”useTabChar”」という設定で制御しているらしく、Brackets – 環境設定 で「Brackets.json」ファイル(Bracketsの設定ファイル)を開いた状態で、上記の「タブ幅」から「スペース」に切り替えると、「”useTabChar”」の値がtrueからfalseにリアルタイムに変わる。

Rails5で自作クラスの読み込み(app配下に置く)

自作のクラスやモジュールを読み込む場合、 Rails4だと

config.autoload_paths += %W(#{Rails.root}/lib)

みたいなのをconfig/application.rbに設定したかと思いますが、
Rails5ではこれを記述するとエラーが出ます。

■対応策 A案
 app配下は自動で読み込まれるため、そこに自作クラスを置く。
 (app配下にあるとわかりやすい&挙動調べるときはまずここを見るかと思いますので
  個人的には素直にここが最適かと思います)

以下、設定例

[app/lib/foo_bar.rb] libフォルダとfoo_bar.rbは自分で作る
class FooBar
  def hi
    return “hi.”
  end

  def bye
    return “Good bye!”
  end
end

[app/controllers/users_controller.rb] 例のためcontrollerかましています
  def index
    @foo = FooBar.new
  end

[app/views/users/index.html.erb]
<%= debug @foo.hello %> #=> hi
<%= debug @foo.bye %> #=> Good bye!

■対応策 B案
 任意の場所libフォルダとかに自作クラスを置いてrequireで読み込む。

 以下、設定例

[lib/hoge.rb] foo_bar.rbは自分で作る
class Hoge
  def hey
    return “hey there.”
  end
end

[app/controllers/users_controller.rb] 例のためcontrollerかましています
  require ‘./lib/hoge’

  def index
    @hoge = Hoge.new
  end

[app/views/users/index.html.erb]
<%= debug @hoge.hey %> #=> hey there.

■対応策 C案
 Rails4みたいにautoloadする設定を入れる。

Rails5のproduction環境でlib/配下のクラス読込みがNameErrorになるのはautoloadが無効化されたからだった

■参考サイト
Rails 5 ignoring /lib class? 
 上記の回答では、config/application.rbでrequireしろ、という内容です
Railsアプリのモジュールはどこに置くべきか問題 (公開版)
・読み込むための命名規則(ファイル名、設置場所に伴うクラス名・メソッド名)が厳密に定義されているので要注意

Rails5でMaterialize使うときのSubmitボタン実装方法

Materializeのsubmitボタンメモ

(1)form.submitはaタグのリンク範囲おかしくなる(文字しかaタグ効かない)からbutton_tag使う
(2)Enterでsubmitしないようにtype:”button”を使う
(3)type:”button”だとsubmitできないのでonclick:”submit();”を定義する

【viewのsubmitボタン実装例】
    <%= button_tag ‘投稿’, class: “btn waves-effect waves-light”, type: “button”, onclick: “submit();” %>

■参考
Input Submit Field with Materialize doesn’t work properly #1493
Enterキーを無効にする方法

Rails5 app/assets、lib/asses、vendor/assetsの使い分け

以下の理解。

・app/assetsはアプリ全体として共通管理するもの(app/assets/javascripts/application.jsとか)やアプリ固有のもの(app/assets/javascripts/posts.coffeeとか)を置く
・lib/assetsは自作コードを置く(特定のviewだけxxのファイルを読み込みたい、みたいな時はここに置いて読み込む?ここ以外に最適そうなとこがない)
・vendor/assetsはサードパーティ(外部ベンダーが提供したもの、bootstrapとか)を置く

—Railsガイドからの引用—

  • app/assetsは、カスタム画像ファイル、JavaScript、スタイルシートなど、アプリケーション自身が保有するアセットの置き場所です。
  • lib/assetsは、1つのアプリケーションの範疇に収まらないライブラリのコードや、複数のアプリケーションで共有されるライブラリのコードを置く場所です。
  • vendor/assetsは、JavaScriptプラグインやCSSフレームワークなど、外部の団体などによって所有されているアセットの置き場所です。

引用元:アセットパイプライン | Rails ガイド

■外部ベンダー提供のjs, CSSを読み込む場合の例
外部ベンダーの bootstrap-tagsinput.min.js と bootstrap-tagsinput.cssを読み込みたいとする。

1.vendor/assets/javascripts/bootstrap-tagsinput.min.js と vendor/assets/stylesheets/bootstrap-tagsinput.css にファイル置く
2.config/initializers/assets.rb に以下を定義する

Rails.application.config.assets.precompile += [
    #Bootstrap Tags Input
    ‘bootstrap-tagsinput.min.js’,
    ‘bootstrap-tagsinput.css’
]

3.railsを再起動する
4.読み込みたいviewで、stylesheet_link_tag と javascript_include_tag を使って定義したファイルを読み込む