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

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

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

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 認可が必要なエンドポイントのテストとかもあって、そのあたりのことも書きたかったのですが、それはまた次の機会に。