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

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

factory_botでactive_model_serializers用のPORO (Plain-Old Ruby Object)のテストデータを作成する

最近、DB の存在しない Rails プロジェクト下で API を作る機会がありました。

外部から fetch してきたデータから PORO (Plain-Old Ruby Object)を作って、 active_model_serializersでひたすらシリアライズしまくるみたいな感じです。 特にテストまわりで若干の工夫が必要だったので、その時のメモです。

環境

現時点でまだ本リリースされていない Rails 6 を無駄に使っている。

構成

  • app/models
    • PORO を定義していく
  • app/serializers
  • app/controllers
    • エンドポイントを定義していく

モデル

サンプルとして URL とサイズの情報を持つImageモデルを定義してシリアライズするケースを考えてみる。

こちらに書かれているように active_model_serializers では PORO 用に ActiveModelSerializers::Modelを定義してくれているので、シンプルなものであればこれを使って簡単に実装できる。

よりファンキーな実装が求められる状況であっても、 こちらの仕様に沿った自前モデルさえ作れれば問題なくワークすると思う。

# app/models/image.rb
class Image < ActiveModelSerializers::Model
  attributes :url, :size
end

# app/models/image/size.rb
class Image::Size < ActiveModelSerializers::Model
  attributes :width, :height
end

リアライザ

# app/serializers/image_serializer.rb
class ImageSerializer < ActiveModel::Serializer
  attributes :url, :type
  has_one :size
end

# app/serializers/image/size_serializer.rb
class Image::SizeSerializer < ActiveModel::Serializer
  attributes :width, :height
end

has_oneみたいにリレーションを定義しておくと、include オプションとかが良い感じに使える。

例えば、 render json: image, include: '*'とするとsizeが含まれた JSON が返るし、 render json: image, include: ''とすると sizeが含まれない JSON が返る。

コントローラ

コントローラではインスタンスを作って、そいつをrenderメソッドに渡せば active_model_serializers が最適なシリアライザを見つけてシリアライズ -> JSON を返却してくれる。

実際のプロジェクトでは外部からデータを取ってくる実装 (下の fetch_data_somehowの部分) が一番ツラかったのだが、本題ではないので割愛。

# app/controllers/v1/images_controller.rb
module V1
  class ImagesController < ApplicationController
    def show
      render json: image, include: params[:include]
    end

    private

    def image
      @image ||= Image.new(image_attrs)
    end

    def image_attrs
      @image_attrs ||= fetch_data_somehow
    end
  end
end

テストまわり

ActiveRecordのモデルのテストとかだと factory_botを使って

FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
  end
end
> user = FactoryBot.create(:user)

みたいにさくっとテストデータを用意できると思う。

ただ今回作成した PORO の場合、createメソッドを呼んでも保存する場所がない。また、ActiveModelSerializers::ModelActiveModelをベースにしているのでnewのタイミングで attributesを渡してやらないと attributes = {}アサインされてしまって、そのままだと意図通りにシリアライズされない。

# spec/factories/images.rb
FactoryBot.define do
  factory :image do
    url { Faker::Internet.url }
    size { build(:image_size) }
  end
end

# spec/factories/image/sizes.rb
FactoryBot.define do
  factory :image_size, class: 'Image::Size' do
    width { rand(100..500) }
    height { rand(100..500) }
  end
end
> image = FactoryBot.create(:image)
=> NoMethodError: undefined method `save!`...

> image = FactoryBot.build(:image)
> image.attributes
=> {}

> image.to_json
=> "{}"

factory_bot はイニシャライザをオーバライドできる initialize_withcreateをスキップできる skip_createを提供してくれているので、これらをありがたく使わせてもらい解決できた。

# spec/factories/images.rb
FactoryBot.define do
  factory :image do
    skip_create
    initalize_with { new(attributes) }
    url { Faker::Internet.url }
    size { build(:image_size) }
  end
end

# spec/factories/image/sizes.rb
FactoryBot.define do
  factory :image_size, class: 'Image::Size' do
    skip_create
    initalize_with { new(attributes) }
    width { rand(100..500) }
    height { rand(100..500) }
  end
end
> image = FactoryBot.create(:image)
=> #<Image:...> `build`と同じ結果になり、エラーは発生しない

> image = FactoryBot.build(:image)
> image.attributes
=> { "url" => ..., "size" => ... }

> image.to_json
=> "{\"url\":...,\"size\":...}"

毎回initialize_withskip_createを書くのはだるいので、最終的には以下のような DSL を用意してそいつを使うことにした。

# config/initializers/factory_bot.rb
if defined?(FactoryBot)
  module FactoryBot
    module Syntax
      module Default
        class DSL
          # Custom DSL for ActiveModelSerializers::Model
          # Original: https://github.com/thoughtbot/factory_bot/blob/v5.0.2/lib/factory_bot/syntax/default.rb#L15-L26
          def serializers_model_factory(name, options = {}, &block)
            factory = Factory.new(name, options)
            proxy = FactoryBot::DefinitionProxy.new(factory.definition)
            if block_given?
              proxy.instance_eval do
                skip_create
                initialize_with { new(attributes) }
                instance_eval(&block)
              end
            end
            FactoryBot.register_factory(factory)

            proxy.child_factories.each do |(child_name, child_options, child_block)|
              parent_factory = child_options.delete(:parent) || name
              serializers_model_factory(child_name, child_options.merge(parent: parent_factory), &child_block)
            end
          end
        end
      end
    end
  end
end
# spec/factories/images.rb
FactoryBot.define do
  serializers_model_factory :image do
    url { Faker::Internet.url }
    size { build(:image_size) }
  end
end

あとは以下のようにテストで使える。

# spec/serializers/image_serializer_spec.rb

require 'rails_helper'

RSpec.describe ImageSerializer, type: :serializer do
  let(:resource) { ActiveModelSerializers::SerializableResource.new(model, options) }
  let(:model) { build(:image) }
  let(:options) { { include: '*' } }

  describe '#url' do
    subject { resource.serializable_hash[:url] }

    it { is_expected.to eq model.url }
  end
  ...
end

まとめ

PORO かわいい。

参照

既存のGitリポジトリのコミットをベースに新規にGitリポジトリを作成する

たまたまこういうことをする必要があったのでメモです。

(例) すでに運用中の bar リポジトリの既存のコミットを initial commit として、新規に foo リポジトリを作成するケース

$ cd /path/to/bar
$ git remote add foo {foo's URL}  # Add foo's remote repository
$ git checkout -B foo $(git rev-list HEAD --max-parents=0) # Create foo branch from the initial commit of bar
$ git merge origin --squash  # Checkout all file changes
$ git commit --amend -C HEAD
$ git push foo HEAD:master

まとめ

git rev-list知らなかった...

Golang + echoなREST APIサーバで、テスト実行時に自動でAPIドキュメントを生成できるようにする

最近、Golang (+echo) で REST API サーバを開発する機会があったのですが、テストを書いたら API ドキュメントを自動生成するような仕組みを作るために試行錯誤したのでメモです。

方針

  • API ドキュメントの生成にはtest2docを利用
    • テストを実行すると API Blueprint 形式でファイルを自動生成してくれそう
    • 該当するメソッドの上にコメントを書くことで最低限の説明は記述できそう
    • README には gorilla/muxjulienschmidt/httprouterのサンプルしか載っておらず echoでうまく動くかは試してみるしかなさそう
  • テストから生成された .apibファイルをaglioみたいなツールにかませば HTML ファイルとして API ドキュメントができそう

プロジェクト構成

github.com/danimal141/rest-api-sampleという名前で実装していく。とりあえずユーザー一覧を返すようなエンドポイント /api/v1/usersを実装して、API ドキュメントを自動生成する方法を考える。

余談だが、Golang のパッケージ依存管理にdepを使ってみたので、それ関連のファイルも混ざっている。

.
├── Gopkg.lock
├── Gopkg.toml
├── api
│   ├── all.apib
│   ├── router
│   │   └── router.go
│   ├── v1
│   │   ├── users.go
│   │   ├── users_test.go
│   │   ├── init_test.go
├── doc
├── gulpfile.js
├── main.go
├── models
│   ├── users.go
├── node_modules
├── package.json
└── vendor

API サーバ実装

まずは API サーバをざっと実装してみる。

models/users.go

package models

import "fmt"

type User struct {
    Id       int
    UserName string
}

func SampleUsers() []User {
    users := make([]User, 0, 10)
    for i := 0; i < 10; i++ {
        users = append(users, User{Id: i, UserName: fmt.Sprint("testuser", i)})
    }
    return users
}

ユーザーの Struct を定義。サンプル実装なので DB に保存等はしていない。

api/v1/users.go

package v1

import (
    "errors"
    "fmt"
    "net/http"
    "strconv"

    "github.com/danimal141/rest-api-sample/models"
    "github.com/labstack/echo"
)

type paginationParams struct {
    Pagination string `query:"pagination"`
}

/*

## Query parameter

key        |value  |description
----------:|------:|----------------------------
pagination |false  |ページネーション機能は未実装なのでfalseが必須

*/
func UsersIndex(c echo.Context) error {
    if err := validatePaginationParams(c); err != nil {
        return err
    }
    return c.JSON(http.StatusOK, models.SampleUsers())
}

func UsersShow(c echo.Context) error {
    users := models.SampleUsers()
    id, err := strconv.Atoi(c.Param("user_id"))
    if err != nil {
        return err
    }
    if id > len(users)-1 {
        err := fmt.Errorf("user_id=%d is not found", id)
        return echo.NewHTTPError(http.StatusNotFound, err.Error())
    }
    return c.JSON(http.StatusOK, users[id])
}

func validatePaginationParams(c echo.Context) error {
    p := new(paginationParams)
    if err := c.Bind(p); err != nil {
        return err
    }
    if p.Pagination != "false" {
        err := errors.New("pagination must be false, because pagination is not supported yet")
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    return nil
}

ユーザー一覧情報を返すUserIndexとユーザー詳細情報を返すUsersShowを定義。

後でどのように API ドキュメント反映されるかを確認するため、一覧はページネーションが未実装であることを確認する?pagination=falseが必須であるとする。メソッドの上にコメントをつけているのも後でドキュメントに反映するためである。

api/router/router.go

package router

import (
    "github.com/danimal141/rest-api-sample/api/v1"
    "github.com/labstack/echo"
)

func NewRouter() *echo.Echo {
    e := echo.New()
    e1 := e.Group("/api/v1")
    e1.GET("/users", v1.UsersIndex)
    e1.GET("/users/:user_id", v1.UsersShow)
    return e
}

ルーティングの定義。

main.go

package main

import "github.com/danimal141/rest-api-sample/api/router"

func main() {
    r := router.NewRouter()
    r.Logger.Fatal(r.Start(":8080"))
}

これで go run main.goしてlocalhost:8080/api/v1/users/1などを確認すると JSON が返却されるはずである。

では次にこの API のテストを書いて API Blueprint ファイルを自動生成する仕組みを作ってみる。

テスト

api/v1/init_test.go

package v1_test

import (
    "log"
    "net/http"
    "os"
    "testing"

    "github.com/adams-sarah/test2doc/test"
    "github.com/danimal141/rest-api-sample/api/router"
    "github.com/labstack/echo"
)

var server *test.Server

func TestMain(m *testing.M) {
    var err error
    r := router.NewRouter()
    test.RegisterURLVarExtractor(makeURLVarExtractor(r))
    server, err = test.NewServer(r)
    if err != nil {
        log.Fatal(err.Error())
    }

    // Start test
    code := m.Run()

    // Flush to an apib doc file
    server.Finish()

    // Terminate
    os.Exit(code)
}

func makeURLVarExtractor(e *echo.Echo) func(req *http.Request) map[string]string {
    return func(req *http.Request) map[string]string {
        ctx := e.AcquireContext()
        defer e.ReleaseContext(ctx)
        pnames := ctx.ParamNames()
        if len(pnames) == 0 {
            return nil
        }

        paramsMap := make(map[string]string, len(pnames))
        for _, name := range pnames {
            paramsMap[name] = ctx.Param(name)
        }
        return paramsMap
    }
}

こちらはドキュメント生成に必要な設定等を記述している。

ここで重要なのがvar server *test.Serverで、server.Finish()を呼ぶことでテスト時のリクエスト、レスポンスを元に.apibファイルを生成してくれる。

また test.RegisterURLVarExtractor(makeURLVarExtractor(r))はリクエストの URL に含まれるパラメータ関連の情報を教えてあげるためのもので、これを呼んでおかないとテスト実行時に Panic する。

具体的には /api/v1/users/1というリクエストで/api/v1/users/:user_idのテストをした場合、makeURLVarExtractorの返り値はmap[user_id:1] になる。そして/api/v1/users/{user_id}というエンドポイントのuser_idの Example は1のような情報がドキュメントに反映される。

api/v1/users_test.go

package v1_test

import (
    "net/http"
    "testing"
)

func TestUsersIndex(t *testing.T) {
    url := server.URL + "/api/v1/users?pagination=false"
    res, err := http.Get(url)
    if err != nil {
        t.Errorf("Expected nil, got %v", err)
    }
    if res.StatusCode != http.StatusOK {
        t.Errorf("Expected status code is %d, got %d", http.StatusOK, res.StatusCode)
    }
}

func TestUsersShow(t *testing.T) {
    url := server.URL + "/api/v1/users/1"
    res, err := http.Get(url)
    if err != nil {
        t.Errorf("Expected nil, got %v", err)
    }
    if res.StatusCode != http.StatusOK {
        t.Errorf("Expected status code is %d, got %d", http.StatusOK, res.StatusCode)
    }
}

今回はエラーケースは省略しているが、例えば /api/v1/users?pagination=trueなどとしてテストすればドキュメントにBadRequestな感じで反映される。

ここまでで go test ./api/v1を実行するとapi/v1.apibが作成されるようになる。

api/all.apib

FORMAT: 1A

<!-- include(./v1/v1.apib) -->

一応all.apibを用意して、将来的に./v2/v2.apibなどを追加できるような構成を意識してみた。

API ドキュメント生成

gulpaglioを導入して、テストでapibが更新されるのを Watch して HTML を作成するようにする。

gulpfile.js

const gulp = require('gulp')
const aglio = require('aglio')
const gaglio = require('gulp-aglio')
const rename = require('gulp-rename')
const fs = require('fs')
const includePath = process.cwd() + '/api'
const paths = aglio.collectPathsSync(fs.readFileSync('api/all.apib', {encoding: 'utf8'}), includePath)

gulp.task('build', () =>
  gulp.src('api/all.apib')
    .pipe(gaglio({template: 'default'}))
    .pipe(rename('out.html'))
    .pipe(gulp.dest('doc'))
)

gulp.task('watch', () =>
  gulp.watch(paths, ['build'])
)

gulp.task('default', ['build', 'watch'])

あとはgulpを立ち上げつつ、サーバのテストを実行すればdoc/out.htmlが更新されるようになる。

ちなみに こんな感じのドキュメントが生成される。

まとめ

ほぼtest2docに助けられた感はありますが、テストによる API ドキュメントの自動生成が実現できました。

サンプルコードを一応こちらにあげておきますので、何かしらのお役に立てれば幸いです。

参照

GolangでEnumをフィールドに持つstructをいい感じにjsonエンコード / デコードする

最近、GolangEnum をフィールドに持つ struct をいい感じに json エンコード / デコードしたい衝動に駆られた事があったので、その時のメモです。

普通に json.Marshal する

例えば、以下のような Userの struct を定義したとする。

type UserRole int

const (
    _ UserRole = iota
    Consumer
    Admin
)

func (r UserRole) String() string {
    switch r {
    case Consumer:
        return "consumer"
    case Admin:
        return "admin"
    default:
        return "unknown"
    }
}

type User struct {
    Id   string   `json:"id"`
    Role UserRole `json:"role"`
}

こいつを普通にjson.Marshalしてみる。

func main() {
    u := &User{Id: "abcde", Role: Admin}
    b, err := json.Marshal(u)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b)) // {"id":"abcde","role":2}
}

これが{"id":"abcde","role":"admin"}のような json になってほしかったのだけど、ただjson.Marshalしただけでは{"id":"abcde","role":2}という文字列が得られてツラい思いをした。

Enum の MarshalJSON メソッドを定義する

type Marshaler interface {
        MarshalJSON() ([]byte, error)
}

というinterfaceがあって、MarshalJSONを実装しておけばいい感じに json エンコードしてくれそうだということがわかったので試してみる。

// 上述のコードにこれを追加
func (r UserRole) MarshalJSON() ([]byte, error) {
    return json.Marshal(r.String())
}

今度はちゃんと{"id":"abcde","role":"admin"}が得られた。

ただしMarshalJSONを実装しただけでは、json 文字列を再度 User にしたい時に、 json: cannot unmarshal string into Go value of type main.UserRoleのようなエラーが出てまたツラい思いをする。

func main() {
    u := &User{Id: "abcde", Role: Admin}
    b, err := json.Marshal(u)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b)) // {"id":"abcde","role":2}

    var u2 User
    err = json.Unmarshal(b, &u2)
    if err != nil {
        log.Fatal(err) // json: cannot unmarshal string into Go value of type main.UserRole
    }
    fmt.Println(u2)
}

これに関してはMarshalJSONの時と同様にUnmarshalJSONを実装しておけばよさそう。

func (r *UserRole) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return fmt.Errorf("data should be a string, got %s", data)
    }

    var ur UserRole
    switch s {
    case "consumer":
        ur = Consumer
    case "admin":
        ur = Admin
    default:
        return fmt.Errorf("invalid UserRole %s", s)

    }
    *r = ur
    return nil
}

これで、ちゃんと json から{abcde admin}というUserの struct が得られる。

jsonenums を使う

最初は上記のように頑張って自分でMarshalJSON, UnmarshalJSONを定義していたが、もう少し調べてみたらjsonenumsというライブラリを使えばこれらの作業を自動化してくれるっぽい。というわけで最終的にこれを使い倒す形で落ち着いた。

プロジェクト構成を以下のようにしたとすると、modelsディレクトリ以下でjsonenums -type=UserRoleとするだけでuserrole_jsonenums.goを自動生成してくれる。

.
├── main.go
└── models
    ├── models.go
    └── userrole_jsonenums.go // jsonenumsが生成してくれる

最終的にコードは以下のようになった。

// models.go

package models

type UserRole int

const (
    _ UserRole = iota
    Consumer
    Admin
)

func (r UserRole) String() string {
    switch r {
    case Consumer:
        return "consumer"
    case Admin:
        return "admin"
    default:
        return "unknown"
    }
}

type User struct {
    Id   string   `json:"id"`
    Role UserRole `json:"role"`
}
// userrole_jsonenums.go

// generated by jsonenums -type=UserRole; DO NOT EDIT

package models

import (
    "encoding/json"
    "fmt"
)

var (
    _UserRoleNameToValue = map[string]UserRole{
        "Consumer": Consumer,
        "Admin":    Admin,
    }

    _UserRoleValueToName = map[UserRole]string{
        Consumer: "Consumer",
        Admin:    "Admin",
    }
)

func init() {
    var v UserRole
    if _, ok := interface{}(v).(fmt.Stringer); ok {
        _UserRoleNameToValue = map[string]UserRole{
            interface{}(Consumer).(fmt.Stringer).String(): Consumer,
            interface{}(Admin).(fmt.Stringer).String():    Admin,
        }
    }
}

// MarshalJSON is generated so UserRole satisfies json.Marshaler.
func (r UserRole) MarshalJSON() ([]byte, error) {
    if s, ok := interface{}(r).(fmt.Stringer); ok {
        return json.Marshal(s.String())
    }
    s, ok := _UserRoleValueToName[r]
    if !ok {
        return nil, fmt.Errorf("invalid UserRole: %d", r)
    }
    return json.Marshal(s)
}

// UnmarshalJSON is generated so UserRole satisfies json.Unmarshaler.
func (r *UserRole) UnmarshalJSON(data []byte) error {
    var s string
    if err := json.Unmarshal(data, &s); err != nil {
        return fmt.Errorf("UserRole should be a string, got %s", data)
    }
    v, ok := _UserRoleNameToValue[s]
    if !ok {
        return fmt.Errorf("invalid UserRole %q", s)
    }
    *r = v
    return nil
}
// main.go

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/danimal141/enum-test/models"
)

func main() {
    u := &models.User{Id: "abcde", Role: models.Admin}
    b, err := json.Marshal(u)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b)) // {"id":"abcde","role":"admin"} よい

    var u2 models.User
    err = json.Unmarshal(b, &u2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(u2) // {abcde admin} よい
}

まとめ

Enum をフィールドに持つ struct の json エンコード / デコードはjsonenumsを使うか、自分でMarshalJSON, UnmarshalJSONを定義すればよさそう。 何か他によい方法がありそうでしたらぜひ教えてください。

参考

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パンくずリストを実装したい人はガンガン使っていけば良いのではないでしょうか。