Railsでパンくずリストを実装するためにbreadcrumbs_on_railsを使ってみた
最近、Rails アプリでパンくずリストをいい感じに実装するために、breadcrumbs_on_railsという Gem を使用してみました。
すでにネット上に使ってみた的な記事はちらほら見受けられましたが、自分なりにどのように使ったかなどを書いておきたいと思います。
breadcrumbs_on_rails の基本的な使い方
以下のように Controller 側でadd_breadcrumb
メソッドを呼んでおくと、呼び出した順に@breadcrmbs
に要素がセットされるようになっている。
class PostsController add_breadcrumb 'ホーム', :root_path add_breadcrumb '投稿一覧', :posts_path def index @posts = Post.all end end
後は View 側でrender_breadcrumbs
メソッドを呼べば、@breadcrumbs
の中身をもとにBreadcrumbsOnRails::Breadcrumbs::SimpleBuilder
が要素をレンダリングしてくれるようになっている。html_safe
もしてくれているし、separator
を渡すこともできる。
body = render_breadcrumbs, separator: '>'
でもこのレンダリングは単純にアンカーリンクを生成して ERB で吐いているだけなので、やはりマークアップ自体をカスタマイズしたいという欲求が出てくる。
これはBreadcrumbsOnRails::Breadcrumbs::Builder
を継承した独自 Builder を定義して、そいつのrender
メソッドを実装することで出来そう。
パンくずリストのマークアップをカスタマイズする
以下のような Builder をconfig/initializers
以下に定義して、自分が使いたいテンプレートを使えるようにした。
module BreadcrumbsOnRails module Breadcrumbs class MyBuilder < Builder # 元々protectedで定義されているが、View側で使いたかったのでpublicにしてる public :compute_name, :compute_path def render # 使いたいテンプレートのパスを指定する @context.render '/layouts/breadcrumbs', elements: @elements, builder: self end end end end
/ Slimです - if elements.present? nav ul - elements.each do |elem| - name = builder.compute_name elem - path = builder.compute_path elem li - if path.present? = link_to_unless_current name, path - else = name
ホームへのリンクなど、ほぼ全ての画面で必要なリンクを ApplicationController で定義する
今回、例えば投稿詳細画面にホーム < 投稿一覧 < Awesome post!(投稿詳細)
みたいなリストが表示されるようなケースを想定していて、この中のホーム
はホーム画面以外の全画面で必要になることが予想される。
このような処理はとりあえずApplicationController
にホームリンク用のadd_breadcrumb
を書いてしまって、パンくずリストを表示したくない画面の Controller 内で@breadcrmbs
を消し去るメソッドを呼ぶようにしてみる。
class ApplicationController < ActionController::Base add_breadcrumb 'Home', :root_path def remove_breadcrumbs @breadcrumbs = nil end end
class HomeController < ApplicationController before_action :remove_breadcrumbs end
Symbol で指定できないパスを指定する
root_path
のようなパスではなく、post_path(@post)
のようなものを指定したい場合の話。一例として、画面設計は完全に適当だけど、ホーム < 投稿一覧 < Awesome post!(投稿詳細)< コメント一覧
みたいなものをコメント一覧画面で表示したいとすると、以下のようにすれば指定できる。
module Posts class CommentsController add_breadcrumb '投稿一覧', :posts_path before_action :add_post_breadcrumb add_breadcrumb 'コメント一覧', :post_comments_path def index @comments = Comment.all end private def add_post_breadcrumb add_breadcrumb @post.title, view_context.post_path(@post) end end
まとめ
breadcrumbs_on_rails という Gem は実装も小さいし、特に不満なく使えました。
実際「フルスクラッチでパンくず表示リストを表示する仕組みを実装するぞ」ってなったとしても、結局 breadcrumbs_on_rails みたいな実装になってしまう気もするので、Rails でパンくずリストを実装したい人はガンガン使っていけば良いのではないでしょうか。
Grape + Rails4.2のAPI開発でエンドポイントとエンティティのテストについて考える
最近、Grape + Rails (v4.2.6)で REST な API を開発する機会があったのですが、今回はその開発の中で悩んだ、「Grape のエンドポイントとエンティティのテスト」について書きたいと思います。
前提として Grape ののエンティティは grape-entity
、テストフレームワークはrspec
を採用しているものとします。また API の format は JSON とします。
API の実装
例として、ユーザーがひたすら無意味に投稿しまくるだけのシンプルなアプリケーションを考えてみる。
モデル
# User class User < ActiveRecord::Base has_many :posts, inverse_of: :user validates :name, presence: true end # Post class Post < ActiveRecord::Base belongs_to :user, inverse_of: :posts validates :user, presence: true validates :text, presence: true scope :latest, -> { order created_at: :desc } end
エンティティ
# User module API module V1 module Entities class User < Grape::Entity expose :id expose :name end end end end # Post module API module V1 module Entities class Post < Grape::Entity expose :id expose :text expose :user, using: Entities::User expose :created_at expose :updated_at end end end end
エンドポイント
# /api/v1/postsでPostの一覧が返ることを想定, module API module V1 class Posts < Grape::API resources :posts do desc 'Return all posts' do success API::V1::Entities::Post end get do posts = Post.includes(:user).latest present(posts, with: API::V1::Entities::Post) end end end end end
# /api/v1/postsのレスポンスのサンプル [ { "id": 2, "text": "ひゃっはーー", "user": { "id": 1, "name": "Tom" }, "created_at": "2016-08-09T07:22:35.000Z", "updated_at": "2016-08-09T07:22:35.000Z" }, { "id": 1, "text": "うぇーーい", "user": { "id": 2, "name": "Bob" }, "created_at": "2016-08-08T07:22:35.000Z", "updated_at": "2016-08-08T07:22:35.000Z" } ]
API のテスト
方針
- Grape ではモデルとエンティティを指定して
present
メソッドを呼ぶといい感じに body(今回は JSON)を返してくれる - この
present
メソッドをテスト環境で使えれば、各エンティティ、エンドポイントで返却されるであろう JSON を組み立ててテストできそう - JSON のテストは
json_spec
使えばよさそう - Rspec で
type: :api
を定義して、API 専用ヘルパーメソッドを読み込んでやればよさそう - このあたりを解読すればいけそう
API 用ヘルパー
# rails_helperで読み込んでおく module API module TestHelpers include Rack::Test::Methods include Grape::DSL::InsideRoute private def app Rails.application end def request last_request end def response last_response end def render_api_response *args # Grape::DSL::InsideRouteモジュールをincludeしているのでpresentメソッドをそのまま使える present(*args).to_json.tap do # presentメソッドを呼ぶとbodyがインスタンス変数に入るようになっているが、テストでは返却されるJSONがわかればよいだけなので@bodyは常にnilにしておく @body = nil end end # Override def entity_representation_for entity_class, object, options # presentメソッドの中で呼ばれるメソッド # 実際はここでenvをセットするような処理が入っているが、Rack::Testのenvメソッドと名前があたってArgumentErrorが発生するので無視する entity_class.represent object, options end end end RSpec.configure do |config| config.include API::TestHelpers, type: :api end
エンティティのテスト
require 'rails_helper' RSpec.describe API::V1::Entities::Post, type: :api do describe 'fields' do subject { render_api_response(post, with: described_class) } let(:user) { create(:user) } # FactoryGirlが入っている体でおねがいします let(:post) { create(:post, user: user) } let(:expected_user) { render_api_response(user, with: API::V1::Entities::User) } it do is_expected.to be_json_eql(post.id).at_path 'id' is_expected.to be_json_eql(post.text.to_json).at_path 'text' is_expected.to be_json_eql(post.created_at.to_json).at_path 'created_at' is_expected.to be_json_eql(post.updated_at.to_json).at_path 'updated_at' is_expected.to be_json_eql(expected_user).at_path 'user' end end end
エンドポイントのテスト
require 'rails_helper' RSpec.describe API::V1::Posts, type: :api do describe 'index' do before { send_request } let(:send_request) { get "/api/v1/posts", params.merge(format: :json) } let(:params) { {} } let(:user) { create(:user) } # FactoryGirlが入っている体でおねがいします let!(:posts) { create_list :post, rand(2..4), user: user } let(:search_result) { Post.latest } let(:expected) { render_api_response(search_result, with: API::V1::Entities::Post) } it do expect(response).to be_successful expect(response.body).to be_json_eql expected end end end
まとめ
Grape を使ったのは初めてだったのでベストプラクティスはわからないですが、今回のテスト方針はまあまあ機能したので個人的には悪くなかったんじゃないかなーと思っています。本当は OAuth2 認可が必要なエンドポイントのテストとかもあって、そのあたりのことも書きたかったのですが、それはまた次の機会に。
RubyでネストしたHashをflatな1次元のHashに変換する
{ :foo => "bar", :hello => { :world => "Hello World", :bro => "What's up dude?", }, :a => { :b => { :c => "d" } } }
みたいなネストした Hash があったとして、これを
{ :foo => "bar", :"hello.world" => "Hello World", :"hello.bro" => "What's up dude?", :"a.b.c" => "d" }
にしたい時がきっとあると思います。
今回それを実現するためのメソッドをStack Overflow から丸パクリしたので書いたので、メモ。
こんな感じ。引数にネストした Hash を渡すと目的の Hash が返ってくるはず。
def flatten_hash_from hash hash.each_with_object({}) do |(key, value), memo| next flatten_hash_from(value).each do |k, v| memo["#{key}.#{k}".intern] = v end if value.is_a? Hash memo[key] = value end end
each_with_object
を使う。初期値{}
をセットして、これに欲しい情報をどんどん突っ込んでいって目的の Hash を完成させる。inject
がブロックの最後に評価した値がmemo
にセットされていくのに対して、each_with_object
は常に最初に渡したこのオブジェクトだけを見ていてくれるのでわかりやすい。そして flat になるまでひたすら再帰処理をする。
実際に上述のネストした Hash に対して適用した場合を考えて、処理の流れを追ってみる。
まず
key = :foo
,value = "bar"
。これはvalue
が Hash ではないのでmemo
が{ :foo => "bar" }
となって次へ。次に
key = :hello
,value = { :world => "Hello World", :bro => "What's up dude?" }
。これはvalue
が Hash なので、このvalue
に対してさらにflatten_hash_from
メソッドが呼ばれる。再帰処理の中の
memo
は新たな{}
であり、1 のmemo
とは異なるので注意。まずkey = :world
,value = "Hello World"
。これはvalue
が Hash ではないので、memo
は{ :world => "Hello World" }
となる。key = :bro
についても同様なので、最終的にmemo
は{ :world => "Hello World", :bro => "What's up dude?" }
となり、これが 2 のflatten_hash_from(value)
に返る。3 からさらに
each
処理へ。ここでkey = :hello
になっているので、memo["#{key}.#{k}".intern] = v
とすることで、memo
のkey
がhello.world
,hello.bro
のようになる。最終的にこの処理の後のmemo
は{ :foo => "bar", :"hello.world" => "Hello World", :"hello.bro" => "What's up dude?" }
になる。次に
key = :a
,value = { :b => { :c => "d" } }
。これもvalue
が Hash なので再帰処理へ。key = :b
,value = { :c => "d" }
なのでさらに再帰処理へ。key = :c
,value = "d"
なので{ :c => "d" }
が 6 のflatten_hash_from
に返る。{ :"b.c" => "d" }
となり、5 のflatten_hash_from
に返る。key = :a
の階層に戻ってきてeach
するので{:"a.b.c" => "d" }
が新たにmemo
に追加される。その結果、memo
は{ :foo => "bar", :"hello.world" => "Hello World", :"hello.bro" => "What's up dude?", :"a.b.c" => "d" }
となるので終了。
まとめ
再帰処理はちゃんと処理の流れを理解して使っていきたい。
参考
Golangで画像をアップロードして表示するだけのアプリをつくってみた
完全に出遅れた感満載だけど、最近 Golang の勉強を始めました。
ちょうど昔、node.js の勉強で画像をアップロードして表示するだけのアプリをつくったことがあったので、今回はそれの Golang 版をつくってみました。
成果物はこちら。
初期表示
とりあえず/
にアクセスしたら画像をアップロードするためのフォームと送信ボタンを表示できるようにしてみる。
Golang は標準でテンプレート機能をサポートしているっぽかったのでそれを使ってみる。
templates/index.html
を用意して以下のようにする。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>{{ .Title |html }}</title> </head> <body> <form action="/upload" enctype="multipart/form-data" method="post"> <input type="file" name="upload" id="upload" multiple="multiple"> <input type="submit" value="Upload file" /> </form> </body> </html>
完全にただの HTML だったので、テンプレ感を出すために{{ .Title }}
の部分にタイトルの情報を渡して表示するようにしてみた。たぶんこんなの必要ない。
次にapp.go
を用意して、次のようにしてみる。(一部抜粋)
var templates = template.Must(template.ParseFiles("templates/index.html")) func IndexHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{"Title": "index"} renderTemplate(w, "index", data) } func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) { if err := templates.ExecuteTemplate(w, tmpl+".html", data); err != nil { log.Fatalln("Unable to execute template.") } } func main() { http.HandleFunc("/", IndexHandler) http.ListenAndServe(":8888", nil) }
templates
を定義してこの中で必要なテンプレートファイルをパースしておく。別ファイルに定義したテンプレートを読み込むにはtemplate.ParseFiles
を使う。template.Must
はバリデーションチェックで、指定したテンプレートのエラーがnil
じゃない場合にpanic
を呼ぶっぽい。templates
はtype Template
のポインタ。renderTemplate
メソッドを定義して、その中でtemplates.ExecuteTemplate
を呼んで該当するテンプレートを表示。エラーがあった場合はlog.Fatalln
を呼ぶ。ここに処理が入った場合はexit(1)
で異常終了する。http.HandleFunc
はfunc HandleFunc(pattern string, handler func(ResponseWriter, *Request))
となっていて、パターンとハンドラメソッドを受け取るようになっているので/
のリクエストでIndexHandler
を実行するようにする。http.ListenAndServe
で8888
ポートでサーバ起動。
ひとまずこんな感じでgo run app.go
したらlocalhost:8888
にindex.html
の内容が表示できた。
画像をアップロード
次に画像をアップロードする処理をUploadHandler
で書いていく。
func UploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Allowed POST method only", http.StatusMethodNotAllowed) return } err := r.ParseMultipartForm(32 << 20) // maxMemory if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } file, _, err := r.FormFile("upload") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer file.Close() f, err := os.Create("/tmp/test.jpg") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer f.Close() io.Copy(f, file) http.Redirect(w, r, "/show", http.StatusFound) } func main() { http.HandleFunc("/", IndexHandler) http.HandleFunc("/upload", UploadHandler) http.ListenAndServe(":8888", nil) }
まず
UploadHandler
で受け付けるのはPOST
だけなのでGET
などが来た場合は405 Method Not Allowed
エラーを返す。multipart/form-data
なファイルのアップロード処理をするためにはまずr.ParseMultipartForm
を呼ぶ必要がある。これのインターフェースはfunc (r *Request) ParseMultipartForm(maxMemory int64) error
となっていて、アップロードするファイルはmaxMemory
のサイズのメモリに保存される。もしファイルのサイズがmaxMemory
を超えた場合、残った部分はシステムのテンポラリファイルに保存される。エラーハンドリングはとりあえずエラーが起きたら
http.StatusInternalServerError
を返すようにする。r.FormFile
で、2 で用意したファイルハンドルを取得することができる。今回は HTML の Form のほうでupload
という名前でデータを送信しているのでその名前で取得する。また、defer file.Close()
を呼んでおくことでUploadHandler
の処理を抜ける前に必ずファイルのクローズ処理が行われる事が保証できる。defer
便利。os.Create("/tmp/test.jpg")
で/tmp
以下にtest.jpg
というファイルを作ります(雑につくったので JPG しか対応していません)。このos.Create
の他にos.OpenFile
を使っている例も見受けられたけど、Create
は内部でOpenFile
を使っていたのでやってることは同じっぽい。これもdefer f.Close()
でちゃんと閉じる。io.Copy
でフォームからアップロードされたデータが/tmp/test.jpg
として保存される。/show
にリダイレクトして、/tmp/test.jpg
の内容を表示する
アップロードした画像を表示
アップロードして保存した画像を表示する。
func ShowHandler(w http.ResponseWriter, r *http.Request) { file, err := os.Open("/tmp/test.jpg") defer file.Close() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } img, _, err := image.Decode(file) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeImageWithTemplate(w, "show", &img) } func writeImageWithTemplate(w http.ResponseWriter, tmpl string, img *image.Image) { buffer := new(bytes.Buffer) if err := jpeg.Encode(buffer, *img, nil); err != nil { log.Fatalln("Unable to encode image.") } str := base64.StdEncoding.EncodeToString(buffer.Bytes()) data := map[string]interface{}{"Title": tmpl, "Image": str} renderTemplate(w, tmpl, data) } func main() { http.HandleFunc("/", IndexHandler) http.HandleFunc("/upload", UploadHandler) http.HandleFunc("/show", ShowHandler) http.ListenAndServe(":8888", nil) }
まず
os.Open
で先ほどの画像を取り出す。Open
も内部でOpenFile
を使っている。こちらもクローズ処理を忘れないようにする。image.Decode
で取り出したファイルをデコードする。writeImageWithTemplate
というメソッドを定義。こちらに取り出した Image を渡して、いい感じにテンプレートに出力するよう試みる。new(bytes.Buffer)
で処理を行うためのメモリ領域を確保する。そしてjpeg.Encode
にそれと*img
を渡してエンコード処理。4 でできあがったものを base64 形式にして
string
に変換。それをテンプレートに渡す。data := map[string]interface{}{"Title": tmpl, "Image": str}
は Key がstring
、Value がinterface{}
にしているけど、これは Swift でAnyObject
を渡すみたいなノリなんだろうなと思っている。
これで画像が埋め込まれた形でテンプレートがちゃんと表示できた!
まとめ
Go は標準パッケージが充実しているなーと思った。今まで触ってきた言語と結構違っていてなかなか慣れないけど、楽しい。
次はゴルーチンとかチャネルとか絡めたものを作りたいなー
参考
Swiftで抽象クラスを実装する
Swift では protocol がサポートされていて、protocol extension もできるようになりましたが、抽象クラスは 2015 年 11 月現在サポートされていません。
僕は抽象クラスがほしいなーって思うときがたまにあって、例えばtypealias
を使用している protocol を何かの変数の型に指定しようとすると、protocol can only be used as a generic constraint...
みたいなエラーが出るんですよね。抽象的に実装したいのにできねえ...ってなります。
そういった現状ですが、前回の記事でも使用した RxSwift で抽象メソッドをトリッキーに実装しているのを見て感動したので、真似して抽象クラスをつくってみました。
AbstractClass の実装
protocol AbstractClass: class {} extension AbstractClass { func abstract<T>() -> T { customFatalError("Must be overridden") let dummyValue: T? = nil return dummyValue! } } private func customFatalError(message: String) { fatalError(message) }
まず空の protocol を
AbstractClass
として定義します。つぎに protocol extension でジェネリクスを使用した
abstract
メソッドを定義します。普通に
fatalError
を呼ぶとその後の行が実行されないという旨の警告が出るので、プライベートメソッドとして内部でfatalError
を呼ぶだけのcustomFatalError
メソッドを定義します。3 で用意したメソッドを呼んで override が必要な旨とともに
fatalError
を出しつつ、ダミーの Optional 値を返すことでコンパイルが無理矢理通るようにします。これでfunc foo() -> Int { return self.abstract() }
のように何かの型を返したい抽象メソッドを定義する場合でもコンパイルエラーにはなりません。
実際の使用例はちょっと適当ですが、以下のようなイメージです。
class Employee: AbstractClass { let name: String private let salary: Int init(name: String, salary: Int) { self.name = name self.salary = salary } func work() { return self.abstract() } func getSalary() -> Int { return self.salary } } class Engineer: Employee { override func work() { print("Writing awesome code!!") } }
AbstractClass
に準拠しているので「このクラスは抽象クラスだ」ってぱっと見でわかるし、「self.abstract()
を返しているメソッドは抽象メソッドなんだな」ってなんとなくわかります。
そしてclass Foo<T>: AbstractClass {}
みたいなジェネリクス付きの抽象クラスを定義をして、変数の型にFoo<String>
などと指定しても前述の protocol のようなエラーは出ません。
ただ Java とかだと抽象クラスをインスタンス化しようとするとコンパイルエラーになると思いますが、これはあくまで抽象クラスもどきなのでインスタンス化しようとしてもコンパイルエラーにはなりませんのでご注意を。。
RxSwift の抽象メソッドの実装がさらに進化してた
上記の実装は 1~2 ヶ月前に見て真似したものだったのですが、最近 RxSwift を見なおしてみると、@noreturn
を使うことでより簡潔になっていました。
@noreturn
は呼び出し元に戻ってこない旨を表す属性で例えばfatalError
も
@noreturn public func fatalError(@autoclosure message: () -> String = default, file: StaticString = default, line: UInt = default)
のように@noreturn
なメソッドとして定義されています。これを使えばより簡潔に書けそうです。
以下が書きなおしたバージョン。
protocol AbstractClass: class {} extension AbstractClass { @noreturn func abstract() { fatalError("Must be overridden") } }
class Employee: AbstractClass { let name: String private let salary: Int init(name: String, salary: Int) { self.name = name self.salary = salary } func work() { // return self.abstract() self.abstract() } func getSalary() -> Int { return self.salary } } class Engineer: Employee { override func work() { print("Writing awesome code!!") } }
まとめ
Swift は抽象クラスをサポートしないのかなー。