一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

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使えばよさそう
  • Rspectype: :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 に対して適用した場合を考えて、処理の流れを追ってみる。

  1. まずkey = :foo, value = "bar"。これはvalueが Hash ではないのでmemo{ :foo => "bar" }となって次へ。

  2. 次にkey = :hello, value = { :world => "Hello World", :bro => "What's up dude?" }。これはvalueが Hash なので、このvalueに対してさらにflatten_hash_fromメソッドが呼ばれる。

  3. 再帰処理の中の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)に返る。

  4. 3 からさらにeach処理へ。ここでkey = :helloになっているので、memo["#{key}.#{k}".intern] = vとすることで、memokeyhello.world, hello.broのようになる。最終的にこの処理の後のmemo{ :foo => "bar", :"hello.world" => "Hello World", :"hello.bro" => "What's up dude?" }になる。

  5. 次にkey = :a, value = { :b => { :c => "d" } }。これもvalueが Hash なので再帰処理へ。

  6. key = :b, value = { :c => "d" }なのでさらに再帰処理へ。

  7. key = :c, value = "d"なので { :c => "d" }が 6 のflatten_hash_fromに返る。

  8. { :"b.c" => "d" }となり、5 のflatten_hash_fromに返る。

  9. 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)
}
  1. templatesを定義してこの中で必要なテンプレートファイルをパースしておく。別ファイルに定義したテンプレートを読み込むにはtemplate.ParseFilesを使う。template.Mustはバリデーションチェックで、指定したテンプレートのエラーがnilじゃない場合にpanicを呼ぶっぽい。templatestype Templateのポインタ。

  2. renderTemplateメソッドを定義して、その中でtemplates.ExecuteTemplateを呼んで該当するテンプレートを表示。エラーがあった場合はlog.Fatallnを呼ぶ。ここに処理が入った場合はexit(1)で異常終了する。

  3. http.HandleFuncfunc HandleFunc(pattern string, handler func(ResponseWriter, *Request))となっていて、パターンとハンドラメソッドを受け取るようになっているので/のリクエストでIndexHandlerを実行するようにする。

  4. http.ListenAndServe8888ポートでサーバ起動。

ひとまずこんな感じでgo run app.goしたらlocalhost:8888index.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)
}
  1. まずUploadHandlerで受け付けるのはPOSTだけなのでGETなどが来た場合は405 Method Not Allowedエラーを返す。

  2. multipart/form-dataなファイルのアップロード処理をするためにはまずr.ParseMultipartFormを呼ぶ必要がある。これのインターフェースはfunc (r *Request) ParseMultipartForm(maxMemory int64) errorとなっていて、アップロードするファイルはmaxMemoryのサイズのメモリに保存される。もしファイルのサイズがmaxMemoryを超えた場合、残った部分はシステムのテンポラリファイルに保存される。

  3. エラーハンドリングはとりあえずエラーが起きたらhttp.StatusInternalServerErrorを返すようにする。

  4. r.FormFileで、2 で用意したファイルハンドルを取得することができる。今回は HTML の Form のほうでuploadという名前でデータを送信しているのでその名前で取得する。また、defer file.Close()を呼んでおくことでUploadHandlerの処理を抜ける前に必ずファイルのクローズ処理が行われる事が保証できる。defer便利。

  5. os.Create("/tmp/test.jpg")/tmp以下にtest.jpgというファイルを作ります(雑につくったので JPG しか対応していません)。このos.Createの他にos.OpenFileを使っている例も見受けられたけど、Createは内部でOpenFileを使っていたのでやってることは同じっぽい。これもdefer f.Close()でちゃんと閉じる。

  6. io.Copyでフォームからアップロードされたデータが/tmp/test.jpgとして保存される。

  7. /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)
}
  1. まずos.Openで先ほどの画像を取り出す。Openも内部でOpenFileを使っている。こちらもクローズ処理を忘れないようにする。

  2. image.Decodeで取り出したファイルをデコードする。

  3. writeImageWithTemplateというメソッドを定義。こちらに取り出した Image を渡して、いい感じにテンプレートに出力するよう試みる。

  4. new(bytes.Buffer)で処理を行うためのメモリ領域を確保する。そしてjpeg.Encodeにそれと*imgを渡してエンコード処理。

  5. 4 でできあがったものを base64 形式にしてstringに変換。それをテンプレートに渡す。data := map[string]interface{}{"Title": tmpl, "Image": str}は Key がstringValueinterface{}にしているけど、これは 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)
}
  1. まず空の protocol をAbstractClassとして定義します。

  2. つぎに protocol extension でジェネリクスを使用したabstractメソッドを定義します。

  3. 普通にfatalErrorを呼ぶとその後の行が実行されないという旨の警告が出るので、プライベートメソッドとして内部でfatalErrorを呼ぶだけのcustomFatalErrorメソッドを定義します。

  4. 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 は抽象クラスをサポートしないのかなー。

参考