既存の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/mux
とjulienschmidt/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 ドキュメント生成
gulp
とaglio
を導入して、テストで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エンコード / デコードする
最近、Golang で Enum をフィールドに持つ 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
使えばよさそう - 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 認可が必要なエンドポイントのテストとかもあって、そのあたりのことも書きたかったのですが、それはまた次の機会に。