GolangのgoaでAPIをデザインしよう(バリデーション編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
わかっちゃいるけど、さぼりたいバリデーション
APIは単純なJSONやText返したり、用途は色々あると思います。ですけど、実際返す処理の前にクライアントから飛んでくるリクエストが間違っていないかバリデーションが必要になりますよね。これがだるい!
しかも、プログラム側で実装するだけでも大変なのに、それをドキュメントにどういう条件でOKとするかとか書き出すと、結構細かく記述する必要があり、正直面倒でしかないと思います。そして、仕様変更があれば楽しい修正が始まります。無駄無駄無駄無駄無駄無駄無駄!!!!!!
goaでやったら秒で終わるよ
そこで、今回はgoaでバリデーションすると、幸せになれる一例を示したいと思います。サンプルコードと説明を貼っていくので、この楽さを実感してほしいです。
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
$ go get github.com/tikasan/goa-stater または $ ghq get github.com/tikasan/goa-stater
デザイン定義
以下のような感じで、用意されているメソッドを使えば大体なんとかなります。
ちなみにFormay関数には、email以外にもいくつかあります。
- “date-time”: RFC3339 date time
- “email”: RFC5322 email address
- “hostname”: RFC1035 internet host name
- “ipv4”, “ipv6”, “ip”: RFC2373 IPv4, IPv6 address or either
- “uri”: RFC3986 URI
- “mac”: IEEE 802 MAC-48, EUI-48 or EUI-64 MAC address
- “cidr”: RFC4632 or RFC4291 CIDR notation IP address
- “regexp”: RE2 regular expression
var _ = Resource("validation", func() { BasePath("/validation") Action("validation", func() { Description("Validation") Routing( GET("/"), ) Params(func() { // Integer型 Param("id", Integer, "id", func() { Example(1) }) // Integer型かつ1〜10以下 Param("integerType", Integer, "数字(1〜10)", func() { Minimum(0) Maximum(10) Example(2) }) // String型かつ1〜10文字以下 Param("stringType", String, "文字(1~10文字)", func() { MinLength(1) MaxLength(10) Example("あいうえお") }) // String型かつemailフォーマット Param("email", String, "メールアドレス", func() { Format("email") Example("example@gmail.com") }) // String型でEnumで指定されたいずれかの文字列 Param("enumType", String, "列挙型", func() { Enum("A", "B", "C") Example("A") }) // String型で何も指定が無ければ”でふぉ”という文字列が自動でセットされる Param("defaultType", String, "デフォルト値", func() { Default("でふぉ") Example("でふぉ") }) // String型で正規表現で指定したパターンの文字列 Param("reg", String, "正規表現", func() { Pattern("^[a-z0-9]{5}$") Example("12abc") }) // 全て必須パラメーター Required("id", "integerType", "stringType", "email", "enumType", "defaultType", "reg") }) Response(OK, ValidationMedia) Response(BadRequest, ErrorMedia) }) })
MediaTypeの定義
とりあえず、返すだけ。
var ValidationMedia = MediaType("application/vnd.validation+json", func() { Description("example") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("integerType", Integer, "数字(1〜10)", func() { Example(5) }) Attribute("stringType", String, "文字(1~10文字)", func() { Example("あいうえお") }) Attribute("email", String, "メールアドレス", func() { Example("example@gmail.com") }) Attribute("enumType", String, "列挙型", func() { Example("A") }) Attribute("defaultType", String, "デフォルト値", func() { Example("でふぉ") }) Attribute("reg", String, "デフォルト値", func() { Example("12abc") }) }) Required("id", "integerType", "stringType", "email", "enumType", "defaultType", "reg") View("default", func() { Attribute("id") Attribute("integerType") Attribute("stringType") Attribute("email") Attribute("enumType") Attribute("defaultType") Attribute("reg") }) })
コントローラー定義
コントローラーは、バリデーションが完了した値が来た時だけを想定した処理をすればおkです。デザイン定義が完了したら、デザインを元にコードを生成しましょう。
$ goagen bootstrap -d github.com/tikasan/goa-stater/design
package main import ( "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // ValidationController implements the validation resource. type ValidationController struct { *goa.Controller } // NewValidationController creates a validation controller. func NewValidationController(service *goa.Service) *ValidationController { return &ValidationController{Controller: service.NewController("ValidationController")} } // Validation runs the validation action. func (c *ValidationController) Validation(ctx *app.ValidationValidationContext) error { // ValidationController_Validation: start_implement // Put your logic here // ValidationController_Validation: end_implement res := &app.Validation{} res.ID = ctx.ID res.IntegerType = ctx.IntegerType res.StringType = ctx.StringType res.Email = ctx.Email res.EnumType = ctx.EnumType res.DefaultType = ctx.DefaultType res.Reg = ctx.Reg return ctx.OK(res) }
実際に動かしてみよう
$ go run main.go
$ curl -v 'http://localhost:8080/api/v1/validation?id=1&defaultType=&email=satak%40gmail.com&enumType=A&integerType=10&stringType=foo®=12abc' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/validation?id=1&defaultType=&email=satak%40gmail.com&enumType=A&integerType=10&stringType=foo®=12abc HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.validation+json < Date: Sat, 06 May 2017 01:47:26 GMT < Content-Length: 117 < {"id":1,"defaultType":"","email":"satak@gmail.com","enumType":"A","integerType":10,"reg":"12abc","stringType":"foo"} * Connection #0 to host localhost left intact
goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com
GolangのgoaでAPIをデザインしよう(基本編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
goでAPIを作成する場合は、必ずといっていいくらいgoaでやっている私ですが、日本ではあまり使っている人が居ません。恐らくこの理由は日本語の情報があまり無いことやシンプルなサンプル集がないからかもしれない・・・と思いまして、軽量なサンプルを定期的に紹介していくことにしました。
API設計フェーズ
goaは最初に設計を行ってから、その設計書を元に実装を行っていきます。設計はDSLと呼ばれるもので、最初はこの定義に慣れないですが、読み方さえ分かれば容易にAPIを設計出来ます。
何が嬉しいの?
例えば、GET /users/:ID
というエンドポイントがあるとします。どのようなロジックを組む必要があるでしょう?
例 curl http://localhost/users/1
でリクエストする
/users/1
の1の部分を取得する- :IDから取得したものが文字なのか数字なのかチェックする
- 取得した:IDを使って、データベースなりにSELECT句などで検索する
- 該当するものがなかったら、404ステータスを返す
- 正しく取得できたら、200ステータスとレコードを返す。
goaだとどうなるか?
/users/1
の1の部分を取得する:IDから取得したものが文字なのか数字なのかチェックする- 取得した:IDを使って、データベースなりにSELECT句などで検索する
- 該当するものがなかったら、404ステータスを返す
- 正しく取得できたら、200ステータスとレコードを返す。
1,2に当たる値チェックなどが一切必要なくなります。上記のように少ないパラメータならそこまでの労力ではありませんが、正規表現が必要だったり、複数だったりすると・・・面倒ですよね?
つまり、goaはビズネスロジックオンリーの開発が出来るようになるので、人が組むコードの部分が非常にシンプルになります。
しかも、swaggerのドキュメントが自動生成されるので、ドキュメント更新地獄から解放されます。
フォルダ構成
以下のようなファイル構成を構築してください。
※designフォルダ内のファイルは実際は一つでもokですが、分ける方が私は好きです。
. ├── LICENSE ├── Makefile ├── README.md ├── design │ ├── api_definition.go │ ├── media_types.go │ └── resources.go └── public └── swagger
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
go get github.com/tikasan/goa-stater または ghq get github.com/tikasan/goa-stater
デザインする
準備が整ったので、さっそくデザインをしていきたいと思います。
今回はとりあえず動かそうということで、サンプルソースをひたすら貼ります。
細かい説明は順を追って記事を作成して、紹介致します。
APIの基本情報(api_definition.go)
APIの実際の動作ではなく、どういったAPIか?やグローバルな設定などを行います。ここに紹介しているもの以外にも関数は存在します。
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = API("goa simple sample", func() { // APIのタイトル Title("tikasan/goa-simple-sample") // APIの説明 Description("goaのサンプルです") // 作成者へのコンタクト情報 Contact(func() { Name("pei") Email("satak47cpc@gmail.com") URL("https://github.com/tikasan/goa-simple-sample/issues") }) // APIのライセンス License(func() { Name("MIT") URL("https://github.com/tikasan/eventory/blob/master/LICENSE") }) // APIのドキュメント Docs(func() { Description("wiki") URL("https://github.com/tikasan/goa-simple-sample/wiki") }) // ホストの設定 Host("localhost:8080") // 対応しているプロトコル定義、httpかhttpsまたはその両方 Scheme("http", "https") // 全てのエンドポイントのベースパス // /usersというエンドポイントがあったら、/api/v1/usersとなる BasePath("/api/v1") // CORSポリシーの定義 Origin("http://localhost:8080/swagger", func() { //クライアントに公開された1つ以上のヘッダー Expose("X-Time") // 1つまたは複数の許可されたHTTPメソッド Methods("GET", "POST", "PUT", "DELETE") //プリフライト要求応答をキャッシュする時間 MaxAge(600) // Access-Control-Allow-Credentialsヘッダーを設定する Credentials() }) })
MediaType(media_types.go)
レスポンスデータの形式を定義します。(詳しいことは別記事を紹介予定)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) // レスポンスデータの定義 // MediaTypeに名前をつけます var IntegerMedia = MediaType("application/vnd.integer+json", func() { // 説明 Description("example") // どのような値があるか(複数定義出来る) Attributes(func() { // idはInteger型 Attribute("id", Integer, "id", func() { // 返すレスポンスの例 Example(1) }) // レスポンスに必須な要素(基本は全て必須にした方が楽) Required("id") }) // 返すレスポンスのフォーマット(別記事で紹介予定) View("default", func() { Attribute("id") }) })
リソースへの操作(resources.go)
リソースへの操作を定義します。(詳しいことは別記事を紹介予定)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) // /actionsの定義をする var _ = Resource("actions", func() { // actionsリソースのベースパス BasePath("/actions") /* リソースに対してどういった操作を行うか定義する add リソースを追加する list リソースをリストで取得する delete リソースを削除する 上記のような感じで定義すればおkです。 */ Action("ping", func() { // アクションの説明 Description("サーバーとの導通確認") Routing( // エンドポイント -> GET http://localhost/api/v1/actions/pingになる GET("/ping"), ) // 返したいレスポンス // 200 OK + MessageTypeで定義しているMediaType Response(OK, MessageType) // 400 BadRequest + ErrorMediaというデフォルトで容易されているMediaType // 足りないパラメーターなどがあれば自動的に返される Response(BadRequest, ErrorMedia) }) Action("hello", func() { Description("挨拶する") Routing( GET("/hello"), ) // リクエストで付加出来るパラメーター Params(func() { // nameという名前でパラメーターをStringでなげれる Param("name", String, "名前", func() { // もし、空だった場合は空文字を格納する Default("") }) // 必ず設定されるべきパラメーター(デフォルト値があるので、存在しなければ空になる) Required("name") }) Response(OK, MessageType) Response(BadRequest, ErrorMedia) }) Action("ID", func() { Description("複数アクション(:id)") Routing( // エンドポイントにリソースを指定出来る // GET http://localhost:8080/api/v1/actions/1になる GET("/:id"), ) Params(func() { // :IDはIntegert型でなければならない。 Param("id", Integer, "id") // Requiredはリソースを含めたエンドポイントになるので、定義しなくても良い //Required("id") }) Response(OK, IntegerType) // 指定したリソースが無ければNotFoundを返す可能生がある Response(NotFound) Response(BadRequest, ErrorMedia) }) }) // Swaggerをローカルで実行するめの定義 var _ = Resource("swagger", func() { Origin("*", func() { Methods("GET") }) Files("/swagger.json", "swagger/swagger.json") Files("/swagger/*filepath", "public/swagger/") })
コードを生成する
インストール方法やコマンドの説明はikawahaさんの記事がわかりやすいので、紹介しておきます。
ikawaha.hateblo.jp
今回はgithub.com/tikasan/goa-stater
のrepoをgo getした想定で説明します。
goagen bootstrap -d github.com/tikasan/goa-stater/design
コマンドを実行すると以下のようなログが流れます。 実行出来ない場合は、designパッケージ(フォルダ)の指定が間違っています。
app/contexts.go app/controllers.go ..... ..... swagger swagger/swagger.json swagger/swagger.yaml
出来上がる構成は以下のような感じです。
実際に作業をするファイルは一部です。
app,client,tool,swagger周りは自動で生成されたもので、変更することはないです。
. ├── LICENSE ├── Makefile ├── README.md ├── actions.go <--------- actionsリソース ├── app ├── client ├── design │ ├── api_definition.go │ ├── media_types.go │ ├── resources.go │ └── security.go ├── main.go <------------ Middlewareとかそこらへんやる ├── public │ └── swagger ├── security.go ├── swagger ├── swagger.go └── tool
このままだと少しごちゃごちゃしているので、私はcontrollerというパッケージにまとめたりしています。
. ├── LICENSE ├── Makefile ├── README.md ├── actions.go <--------- controllerフォルダへ移動させる ├── app ├── client ├── controller ├── design │ ├── api_definition.go │ ├── media_types.go │ ├── resources.go │ └── security.go ├── main.go ├── public │ └── swagger ├── security.go ├── swagger ├── swagger.go <------------ controllerフォルダへ移動させる └── tool
main.goを若干記述を変える必要があるので、調整だけしてください。
//go:generate goagen bootstrap -d github.com/tikasan/goa-stater/design package main import ( "github.com/goadesign/goa" "github.com/goadesign/goa/middleware" "github.com/tikasan/goa-stater/app" "github.com/tikasan/goa-stater/controller" <-------importする ) func main() { // Create service service := goa.New("goa simple sample") // Mount middleware service.Use(middleware.RequestID()) service.Use(middleware.LogRequest(true)) service.Use(middleware.ErrorHandler(service, true)) service.Use(middleware.Recover()) // Mount "actions" controller c := controller.NewActionsController(service) <-----controllerパッケージからメソッドを呼ぶ app.MountActionsController(service, c) // Start service if err := service.ListenAndServe(":8080"); err != nil { service.LogError("startup", "err", err) } }
これで準備完了しました。ここまでの作業は正直テンプレートなので、慣れればほとんど時間がかかりません。
ビジネスロジックを組んでみよう
package controller import ( "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // ActionsController implements the actions resource. type ActionsController struct { *goa.Controller } // NewActionsController creates a actions controller. func NewActionsController(service *goa.Service) *ActionsController { return &ActionsController{Controller: service.NewController("ActionsController")} } // Action ID func (c *ActionsController) ID(ctx *app.IDActionsContext) error { /* Paramsで定義したものはctx.hogeで受け取れる Params(func() { Param("ID", Integer, "ID") }) */ if ctx.ID == 0 { // Response(NotFound) // 0の場合は、404エラーを返す return ctx.NotFound() } // Response(OK, IntegerType) // MediaType IntegerTypeでレスポンス res := &app.Integer{} /* IDはInteger型で Attribute("ID", Integer, "ID", func() { Example(1) }) */ res.ID = ctx.ID return ctx.OK(res) } // Hello runs the hello action. func (c *ActionsController) Hello(ctx *app.HelloActionsContext) error { // ActionsController_Ping: start_implement // Put your logic here name := ctx.Name // ActionsController_Ping: end_implement res := &app.Message{} res.Message = "Hello " + name return ctx.OK(res) } // Ping runs the ping action. func (c *ActionsController) Ping(ctx *app.PingActionsContext) error { // ActionsController_Ping: start_implement // Put your logic here message := "pong" // ActionsController_Ping: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) }
以上でAPIのビジネスロジックの実装完了です。簡単でしょう?
では、実行してみましょう。
go run main.go
エラーが起きた場合は、importミスやポートが空いてないや何かしらのミスが考えられます。
curl -v http://localhost:8080/api/v1/actions/1 * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/actions/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.integer+json < Date: Fri, 05 May 2017 11:49:59 GMT < Content-Length: 9 < {"id":1} * Connection #0 to host localhost left intact
上記のような感じでレスポンスが返ってきました。
試しに間違ったリクエストを投げてみましょう。
curl -v http://localhost:8080/api/v1/actions/hoge * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/actions/hoge HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 400 Bad Request < Content-Type: application/vnd.goa.error < Date: Fri, 05 May 2017 12:01:54 GMT < Content-Length: 188 < {"id":"Iwzpk08B","code":"invalid_request","status":400,"detail":"invalid value \"hoge\" for parameter \"id\", must be a integer","meta":{"expected":"integer","param":"ID","value":"hoge"}} * Connection #0 to host localhost left intact
エラーのフォーマットはカスタム出来ますが、いまはデフォルトで定義されているフォーマットで返ってきます。特にこだわりがなければこれで完了します。
SwaggerUIを使ってみよう
swaggerのドキュメントも出来上がっているので、github.com/tikasan/goa-stater
を使っている場合は、以下のコマンドでAPIのドキュメントの参照とテストが出来ます!!!
open http://localhost:8080/swagger/index.html
初めの方に説明した。ビズネスロジックだけを書けば実装が完了していることが実感出来たと思います。他にもヘッダーのチェックやmodel定義が簡単に出来るgormaなど色々あるので、どんどん紹介していきたいと思います。
goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com
GithubのOAuth2.0の仕様について理解する(Githubログイン)
あの楽ちんなGithubログインボタン
結構前からですが、他のSNSサービスや比較的数多くのアカウント登録数を持っているサービスが採用しているOAuth2.0ですけど、自分が関わっているプロダクトで使う必要が出てきたので、仕様と使い方にGithubの公式ドキュメントを読みながらまとめてみた。
OAuth | GitHub Developer Guide
公式ドキュメントの大体合ってる日本語訳
※実際の翻訳と異なることが多少あります。大体そんな感じという日本語訳です。
OAuth2は外部アプリケーションがユーザーのGithubアカウントが持っているプライベートな情報にパスワードなしで情報にアクセスすることが出来るプロトコルです。トークンは特定のタイプのデータに限定でき、ユーザーはそのトークンをいつでも消せるということらしい。このトークン仕組みははBasic認証よりも良いですよ?みらいなことが書いてある。
つまり、トークンとやらを発行して、そのトークンを使うことで許可された範囲で操作出来るよってことらしい。Basic認証よりイケてるからみんな使ってこうぜ!的なことが言いたいのだと思う。
全ての開発者は、これを利用する前にアプリケーションの登録をする必要があります。登録されたOAuthアプリケーションのには、固有のClient IDとClient Secretが割り当てられます。
※このClient Secretは絶対に共有しないでください!
開発者の自身で使用するための個人アクセストークンを作成したり、以下に紹介するフローを実装することで、他のユーザーがアプリケーションを許可することが出来ます。
ここで出たClient Secretは間違って、Githubとかに上げると悪用されるので、公開しないようにしてくださいという意味だと思われる。
GithubのOAuth実装は、authorization code grant typeをサポートしています。開発者は以下で説明するWebアプリケーションフローを実装して、認証コードを取得し、それをトークンに交換する必要があります。(implicit grant typeはサポートしていません)
Webアプリケーションでのフロー
大体こんな感じだと思う。
1.ユーザーはリダイレクトでGithubへアクセスする
GET https://github.com/login/oauth/authorize
設定できるパラメーター
*は必須という意味です。
Name | Type | 説明 |
---|---|---|
client_id * | string | 登録時にい発行されたクライアントID |
redirect_uri | string | 承認後にユーザーのリダイレクト先。(アプリケーションURL) |
scope | string | scopeスペースで区切られたスコープのリスト。scopeは空の場合は、アプリケーションのscopeを許可していないユーザーの空リストにデフォルトで設定されます。アプリケーションのscopeを承認したユーザーの場合は、scopeのリストを含むOAuth認証ページは表示されません。その代わり、このフローでは、ユーザーのアプリケーションに対しての承認を自動的に処理する。 |
state | string | サイト間のやり取りをする際のCSRF対策に使うランダムな文字列。これが漏れると攻撃されるので、取扱には気をつけたい。 |
allow_signup | string | 認証されていないユーザーに、OAuthフロー中にGithubにサインアップするオプションを提供するかどうかを設定する。つまり、認証フローに入る前からGithubにログイン済みのユーザーしか許可しないかどうかを決定するようです。もし、許可する場合はデフォルトで設定されているtrue で、許可したくない場合はfalse とするようです。 |
補足
redirect_uri
リダイレクトのURLには以下のようなルールがあります。
OAuth | GitHub Developer Guide
CALLBACK: http://example.com/path GOOD: http://example.com/path GOOD: http://example.com/path/subdir/other BAD: http://example.com/bar BAD: http://example.com/ BAD: http://example.com:8080/path BAD: http://oauth.example.com:8080/path BAD: http://example.org
示されている例が少し分かりにくかったので、ありそうな例を書きました。
http://localhost/login ◯http://localhost/login/callback ×http://localhost/callback
scope
OAuth | GitHub Developer Guide
このscopeというのは、アプリケーションから取得または操作を許可する範囲のことを指している。
例えば、何も設定をしないno scope
の場合は、公開されている情報に読み取り専用でアクセスを許可されたり、user:email
などにすると、ユーザーがGithubに登録しているメールアドレスへアクセスが出来るなど。様々なレベルのscopeがあるので、必要に応じて設定すると良いらしい。
2.Githubからアプリケーションにリダイレクトする
POST https://github.com/login/oauth/access_token
ユーザーがリクエストを受け入れると、Githubはcode
パラメーターの一時コードと1の手順で指定したstate
をパラメーターに指定したアプリケーションにリダイレクトされます。この時にstateが違った場合は攻撃の可能生が高いため、OAuthのフローを中止するべきです。
設定できるパラメーター
*は必須という意味です。
Name | Type | 説明 |
---|---|---|
client_id* | string | アプリケーション登録時に発行されたclient_id |
client_secret* | string | アプリケーション登録時に発行されたclient_secret |
code* | string | 1の手順の応答で受け取る。tokenとの引き換え券のようなもの。 |
redirect_uri | string | 承認後にユーザーがリダイレクトされるアプリケーションのURL。リダイレクトのルールは先程紹介したものと同じです。 |
POST https://github.com/login/oauth/access_token
へのレスポンス
access_token=e72e16c7e42f292c6912e7710c838347ae178b4a&scope=user%2Cgist&token_type=bearer
Accept ヘッダーに応じて、様々な形式で受け取ることが出来ます。
Accept: application/json {"access_token":"e72e16c7e42f292c6912e7710c838347ae178b4a", "scope":"repo,gist", "token_type":"bearer"} Accept: application/xml <OAuth> <token_type>bearer</token_type> <scope>repo,gist</scope> <access_token>e72e16c7e42f292c6912e7710c838347ae178b4a</access_token> </OAuth>
3. tokenを使ってAPIへリクエストする
ここまでのフローで受け取ったaccess_tokenでAPIへリクエストが出来ます。
tokenは以下のようなパラメーターで渡すパターン。
GET https://api.github.com/user?access_token=...
HeaderのAuthorizationを使って渡すことも出来ます。
Authorization: token OAUTH-TOKEN
curlコマンドでは以下のようになります。
curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com/user
なんとなく分かった気がする。
GoでAPIから取得したJSONを5分でパースする
微妙に面倒なアレ
GoはAPIの用途で、私は結構使うのですが、そのAPIを構築する上で、外部のAPIを使ってデータを集めたりすることもよくあります。そして避けて通れないのが、JSON解析です。自力でやると地味に面倒です。
今回はその作業は5分で終わらせる方法を紹介します。
はよソース
便利なツール達
https://mholt.github.io/json-to-go/JSON-to-Go: Convert JSON to Go instantly
JSONを食わせると、解析するstructを生成してくれる。JSON Pretty Linter - JSONの整形と構文チェック
JSONを整形してくれる。
Let’s try!!!
目的のJSONを取得する
connpass.com 今回はConnpassのAPIから返ってくるJSONを解析します。
{ "results_returned": 1, "events": [ { "event_url": "https://kikaigakushuu.connpass.com/event/56040/", "event_type": "participation", "owner_nickname": "keiichirou_miyamoto", "series": { "url": "https://kikaigakushuu.connpass.com/", "id": 2589, "title": "人工知能勉強会" }, "updated_at": "2017-04-26T02:47:01+09:00", "lat": "35.696575200000", "started_at": "2017-05-09T20:00:00+09:00", "hash_tag": "geek", "title": "強くなるロボティック・ゲームプレイヤーの作り方勉強会「4章 強化学習 前半」", "event_id": 56040, "lon": "139.771830100000", "waiting": 0, "limit": 30, "owner_id": 10866, "owner_display_name": "keiichirou_miyamoto", "description": "説明", "address": "東京都千代田区神田須田町2丁目19−23 (野村第3ビル4階)", "catch": "#人工知能,#機械学習,#深層学習,#ニューラルネット,#ディープラーニング,#初心者", "accepted": 17, "ended_at": "2017-05-09T22:00:00+09:00", "place": "コワーキングスペース秋葉原 Weeyble(ウィーブル)" } ], "results_start": 1, "results_available": 1744 }
https://connpass.com/api/v1/event/?keyword=python&count=1
リクエストをブラウザで実行します。
JSON Pretty Linter - JSONの整形と構文チェック コピーして、JSON整形します。 整形したJSONをコピーして、goのソースに貼り付けます。
jsonStrという変数に以下のような感じで代入してください。
var jsonStr = `{ "results_returned": 1, "events": [ { "event_url": "https://kikaigakushuu.connpass.com/event/56040/", "event_type": "participation", "owner_nickname": "keiichirou_miyamoto", "series": { "url": "https://kikaigakushuu.connpass.com/", "id": 2589, "title": "人工知能勉強会" }, "updated_at": "2017-04-26T02:47:01+09:00", "lat": "35.696575200000", "started_at": "2017-05-09T20:00:00+09:00", "hash_tag": "geek", "title": "強くなるロボティック・ゲームプレイヤーの作り方勉強会「4章 強化学習 前半」", "event_id": 56040, "lon": "139.771830100000", "waiting": 0, "limit": 30, "owner_id": 10866, "owner_display_name": "keiichirou_miyamoto", "description": "説明", "address": "東京都千代田区神田須田町2丁目19−23 (野村第3ビル4階)", "catch": "#人工知能,#機械学習,#深層学習,#ニューラルネット,#ディープラーニング,#初心者", "accepted": 17, "ended_at": "2017-05-09T22:00:00+09:00", "place": "コワーキングスペース秋葉原 Weeyble(ウィーブル)" } ], "results_start": 1, "results_available": 1744 }`
JSONからstructを作成する
https://mholt.github.io/json-to-go/JSON-to-Go: Convert JSON to Go instantly
jsonを食わせて、strcutを生成する。
右に出てるstrcutをコピーしてgoのソースに貼り付ける
type AutoGenerated struct { ResultsReturned int `json:"results_returned"` Events []struct { EventURL string `json:"event_url"` EventType string `json:"event_type"` OwnerNickname string `json:"owner_nickname"` Series struct { URL string `json:"url"` ID int `json:"id"` Title string `json:"title"` } `json:"series"` UpdatedAt time.Time `json:"updated_at"` Lat string `json:"lat"` StartedAt time.Time `json:"started_at"` HashTag string `json:"hash_tag"` Title string `json:"title"` EventID int `json:"event_id"` Lon string `json:"lon"` Waiting int `json:"waiting"` Limit int `json:"limit"` OwnerID int `json:"owner_id"` OwnerDisplayName string `json:"owner_display_name"` Description string `json:"description"` Address string `json:"address"` Catch string `json:"catch"` Accepted int `json:"accepted"` EndedAt time.Time `json:"ended_at"` Place string `json:"place"` } `json:"events"` ResultsStart int `json:"results_start"` ResultsAvailable int `json:"results_available"` }
UmarshalでJSONをパースする
jsonパッケージのUnmarshalでパースして終了。
jsonBytes := ([]byte)(jsonStr) data := new(AutoGenerated) if err := json.Unmarshal(jsonBytes, data); err != nil { fmt.Println("JSON Unmarshal error:", err) return } fmt.Println(data.Events[0])
$ go run main.go
以上で、完了です!!!簡単すぎる!!!!
全ソース
package main import ( "encoding/json" "fmt" "time" ) type AutoGenerated struct { ResultsReturned int `json:"results_returned"` Events []struct { EventURL string `json:"event_url"` EventType string `json:"event_type"` OwnerNickname string `json:"owner_nickname"` Series struct { URL string `json:"url"` ID int `json:"id"` Title string `json:"title"` } `json:"series"` UpdatedAt time.Time `json:"updated_at"` Lat string `json:"lat"` StartedAt time.Time `json:"started_at"` HashTag string `json:"hash_tag"` Title string `json:"title"` EventID int `json:"event_id"` Lon string `json:"lon"` Waiting int `json:"waiting"` Limit int `json:"limit"` OwnerID int `json:"owner_id"` OwnerDisplayName string `json:"owner_display_name"` Description string `json:"description"` Address string `json:"address"` Catch string `json:"catch"` Accepted int `json:"accepted"` EndedAt time.Time `json:"ended_at"` Place string `json:"place"` } `json:"events"` ResultsStart int `json:"results_start"` ResultsAvailable int `json:"results_available"` } func main() { var jsonStr = `{ "results_returned": 1, "events": [ { "event_url": "https://kikaigakushuu.connpass.com/event/56040/", "event_type": "participation", "owner_nickname": "keiichirou_miyamoto", "series": { "url": "https://kikaigakushuu.connpass.com/", "id": 2589, "title": "人工知能勉強会" }, "updated_at": "2017-04-26T02:47:01+09:00", "lat": "35.696575200000", "started_at": "2017-05-09T20:00:00+09:00", "hash_tag": "geek", "title": "強くなるロボティック・ゲームプレイヤーの作り方勉強会「4章 強化学習 前半」", "event_id": 56040, "lon": "139.771830100000", "waiting": 0, "limit": 30, "owner_id": 10866, "owner_display_name": "keiichirou_miyamoto", "description": "説明", "address": "東京都千代田区神田須田町2丁目19−23 (野村第3ビル4階)", "catch": "#人工知能,#機械学習,#深層学習,#ニューラルネット,#ディープラーニング,#初心者", "accepted": 17, "ended_at": "2017-05-09T22:00:00+09:00", "place": "コワーキングスペース秋葉原 Weeyble(ウィーブル)" } ], "results_start": 1, "results_available": 1744 }` jsonBytes := ([]byte)(jsonStr) data := new(AutoGenerated) if err := json.Unmarshal(jsonBytes, data); err != nil { fmt.Println("JSON Unmarshal error:", err) return } fmt.Println(data.Events[0]) }
実際にAPIにリクエストする
package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "time" ) type AutoGenerated struct { ResultsReturned int `json:"results_returned"` Events []struct { EventURL string `json:"event_url"` EventType string `json:"event_type"` OwnerNickname string `json:"owner_nickname"` Series struct { URL string `json:"url"` ID int `json:"id"` Title string `json:"title"` } `json:"series"` UpdatedAt time.Time `json:"updated_at"` Lat string `json:"lat"` StartedAt time.Time `json:"started_at"` HashTag string `json:"hash_tag"` Title string `json:"title"` EventID int `json:"event_id"` Lon string `json:"lon"` Waiting int `json:"waiting"` Limit int `json:"limit"` OwnerID int `json:"owner_id"` OwnerDisplayName string `json:"owner_display_name"` Description string `json:"description"` Address string `json:"address"` Catch string `json:"catch"` Accepted int `json:"accepted"` EndedAt time.Time `json:"ended_at"` Place string `json:"place"` } `json:"events"` ResultsStart int `json:"results_start"` ResultsAvailable int `json:"results_available"` } func main() { url := "https://connpass.com/api/v1/event/?keyword=python&count=1" resp, _ := http.Get(url) defer resp.Body.Close() byteArray, _ := ioutil.ReadAll(resp.Body) jsonBytes := ([]byte)(byteArray) data := new(AutoGenerated) if err := json.Unmarshal(jsonBytes, data); err != nil { fmt.Println("JSON Unmarshal error:", err) return } fmt.Println(data.Events[0]) }
以上で終了です。Go最高っすね。
Go合宿でビール駆動開発してきた@土善旅館
乾杯!乾杯!乾杯!
connpass.com
Go言語の合宿参加してきました。
一言で言うと、アルコール+Gopherって感じでした。
会場について
開発向けのプランがあるというイケてる旅館でした。
以下に気になってる人向けに良かった点をまとめておきます。
- 高機能な電源タップと延長コードが大量にある。
- 大きなスクリーンがある。
- WiFi完備
- スイッチングハブがあったり。。。(本当によくわからないくらいある)
- TCP/IPマスタリング本やけものフレンズなど、偏った本のチョイス。
- コーヒーが飲み放題。
- 温泉がある。(24H)
- 宴会場が60人くらい入れる。
- 人が駄目になるクッションがある。
- ディスプレイがある。(デュアルディスプレイとか出来る。)
- 飯がうまい。
誰でも参加できるイベント?
Go言語に興味がある人やGo言語が好きな人、Go知らないけど楽しいコトしたい!という方なら、誰でも参加出来るイベントです。
参加者の実力も様々で、一切やったことない人から、有名なコミッターの方まで、私のような学生でも問題なく楽しめます。少しでも興味があれば気軽に申し込みをすると良いと思います。
他の参加者の記事
本題
JR日暮里駅で集合して、そこから笹川駅へ!
正直遠かったです。夜行バスからのこの移動は結構体力もってかれましたw
お金に余裕があるなら、新幹線か前日入りまたは飛行機で成田から合流とかした方が良いかもです。
笹川のこのいい感じの田舎感。本当にIT系とはかけ離れてる場所でしたw
合宿所到着!大きなスクリーン!この時点でかなりテンションがあがる。
開発に適した環境が用意されていて、参加者びっくり。
Go合宿スタート!!!(初乾杯)
開発部屋風景。(乾杯)
開発にはビールは必須。当然ですが昼間からビールを飲みます。もちろん開発もしてました。(そして、乾杯)
合宿の形式
基本的にもくもく形式で、個々でテーマを決めて何かしらの成果を出します。そして、2日目にあるLT大会で発表する。参加者の中には初心者もいらっしゃったので、@tenntennさんよるレクチャータイムもありました。
LT大会にはGo華景品もあり、どうせなら頑張っちゃおうって感じで、みなさん頑張ってました。
そして、飯がうまい!!!(既に4回くらい乾杯してる)
飯を食ってからが本番。
晩御飯を食べている時に、参加者のみなさん口を揃えて、「開発?ってなんですか」「これは一度、オフトゥンの様子を確認した方が良さそうです!」とか、「風呂入って酔いを覚まして・・・そこから乾杯するか」など、全くやらないオーラすごかったですが・・・
部屋に戻ると、みなさん真面目に開発していました。そして、何人かの参加者が「ビール入れた方が捗るんだが」と目覚めてる人もちらほら居ましたw
いざ、LT大会!!!(乾杯)
私もLTしました。goaの話に触れつつ、goを好きになる方法について話しました。
以下に資料を貼ったので、興味あればどうぞ。
結果発表
Goを一番愛しているで賞?というものに受賞しました!!!
景品は、@tenntennさんがアメリカでGopherグッズを買ってきてくれるそうで、かなり嬉しいw
景品提供ありがとうございました!!!
合宿終了
運営の素晴らしい進行により、無事大成功で合宿は終了しました。
とても、楽しかったです!ありがとうございました!
どこかで見たことがある集合写真を撮影し、解散しました。
OSSにコントリビュートしたら楽しかった
OSS楽しい!
僕みたいな学生エンジニアに限らず、お世話になることが多い世界中のOSSですが、つい先日ようやくコントリビュート童貞卒業したぺいです。
結構ハードルを感じている人が居ますが、もっと気軽にやってもらいたいと考え、今回は魅力について書いてみたいと思います。
何にコントリビュートしたか?
趣味で作っているアプリのバックエンドに使っているGoのWAFのgoaです。主にバグの修正やGAE対応です。
github.com github.com github.com github.com
説明努力をすれば、周りも助けてくれるかも?
goaの関係者が優しいというのもありますが、どのPRにしても色んな人の助けられてマージに至っています。「直そうと思ったけど、こういう問題があって難しいです」と正直に相手に伝えてみるといいと思います。場合によってはヒントを頂けます。そういったコミュニケーションを繰り返し、改善をしていけばマージ出来るコードになります。なので、実力に自信がなくてコミット出来てない人は、完璧でないといけないと気負いすることをまずやめたら良いと思います。
メリットがたくさん
何かの問題を解決することは、全体の構成を知るためにコードをたくさん読む必要があったり、そもそも言語の問題などが絡んでることもあります。なので、強制的に勉強になることが多く、自分の知識の穴埋めに丁度良いです。また、テクニックなども盗み放題なので、自分のものにすることが出来るのも魅力です。
解決するものによってはかなり大変なものもあります。(バージョンの問題や下位互換など)ですが、そういった問題解決はとても良い経験になり、個人でやっている開発にも応用できます。また、単純に達成感もありすごく楽しいです。なので、興味が少しでもあればチャレンジしてみると良いと思います。
小さいことでも貢献になる
初心者からするとハードルを感じてしまうPRやISSUEなど、「こんなしょうもないこと書いたら馬鹿にされないかな」とか、「間違ってたらどうしよう」とか、色々思うことはあると思いますけど、正直あまり気にしなくてもいいと思います。以下のようなものでも十分な貢献になります。
- READMEやコメントなどのtypo修正
- closeしていないコネクションcloseするだけ
- 処理結果変わらないけど、若干無駄が減る
- バグか分からないけど、怪しい挙動の報告
人気なOSS程、製作者は暇でないことが多いので、気づかないような小さいミスは見過ごしがちなので、結構PRチャンスはあるらしい?また、相手が知らないかもしれないバグを報告することも有益な情報です。特に自分が使っているOSSなら、自分のためにもなるので積極的にコミットすることをおすすめします。
中身のないISSUEはやめよう
Githubは誰でもアカウントが取れるので、気軽にOSS活動が出来ますが、全く利益を生まないようなISSUEを投げるのはやめましょうw 製作者側からしたら読む気が失せるような内容の報告が結構散見されます。相手も人間なので一定のマナーを守ること大事だと思います。
PRによってはテストが必要
コードの修正によって起きる変更に合わせたテストも加えることも必要です。どういうテストが必要なのかどこに書いたらいいかなどは、他のテストを参考にしたり、どうしても分からない時は聞くなりすると良いと思います。
今後も出来る限りコミットを継続し、色んなOSSに携わりたい!
テストコードが生む生産性について
テストコード書いてますか?!!!
最近になって、テストコードをちゃんと書くようになったぺいです。
今まで、どうしても以下のような理由でなかなか書けていませんでした。
- どうやって書けばいいのか分からん。
- 正直めんどい。
何故そんな私が、テストコード書くようになったのかについて、実経験を元にまとめてみました。
テスト書かないと起きる問題
自分はまだ学生ですが、アルバイトなどでプロダクトコードを書く機会があります。そこでテストを書いていないことによって起きた問題がありました。
それは、大きな変更が怖くて出来ない事と、動作チェックを手作業で行うので、チェック漏れはもちろん時間もかかるという2点です。
大きな変更が怖くて出来ない
例えば、様々な箇所から参照されているプロパティやメソッドの変更があったとします。その変更をすることは容易ですが、その変更から生じる影響範囲が広ければ広いほど、絶対に大丈夫という担保が難しくなります。そのため、問題っちゃ問題だけど、思い切って直すまでもないなというものは放置してしまう。
しかし、これが絶対に必要な変更だった場合が最悪です。かなり危険な上に、精神衛生上よろしくない開発がスタートします。もし、テストをちゃんと書いていれば、変更によって生じる問題が分かるので、変更に対して億劫にならずに済み、改善もスムーズに出来るので、最終的にサービスの寿命も生産性も上がると感じました。
手作業チェックはテストコード作成より時間がかかる
テストコードを書かない場合、コードの安全性は手作業によって行われます。なので、当然のように抜けもあります。しかも、チェックがその時の一回しか行われないので、後からの仕様変更でバグが発生しても、気がつかないことがほとんどです。結局そのバグのせいで余計な業務が発生して、テスト書く時間以上の時間がかかっていたりして、かなり悲惨な状態になっていたりする。
また、出来上がったコードのチェックがそもそも面倒なので、コードチェックは後回しになることが多く永遠チェックされないまま放置されたりしていることもある。
結論
テストは書かなくてもプロダクトは作れますが、長く運用する程、問題が発生します。結局テストコードを余裕で書ける時間以上の時間を食われるので、無理してでも書いた方が良いと結論づき、書くようになりましたとさ。
github.com 余談になりますが、BDDというテスト手法がわりと自分の中で良いなと感じいます。