FirebaseUIでTwitterログイン機能を実装する

FirebaseUIはFirebase Authentication SDK上に構築されるライブラリで、アプリで使用するログイン機能とUIを提供してくれるものです。
メールアドレス、FacebookTwitterGitHub、電話番号といった複数のプロバイダに対応しています。
今回はTwitterでログインできるようにします。

Twitter Appsの作成

https://apps.twitter.com/app/new から新しいAppsを作成します。
Callback URLは後で入力します。

f:id:inari111:20180722221015p:plain

Firebase Authenticationの設定を変更する

Authentication -> ログイン方法 からログインプロバイダの設定を変更します。
今回はTwitterを有効にしました。
APIキーとAPIシークレットは ↑で作成したAppsのものを使いましょう。

f:id:inari111:20180722221353p:plain

コールバックURLをTwitter Appに追加

↑でTwitterを有効にするときに表示されたコールバックURLをTwitter Appに追加します。

コード

Firebaseのドキュメントのコードほぼそのままで動きます。 (僕はconfig間違えてて無駄にハマった。。。) configの値は各自の環境に合わせてください。

<script src="https://www.gstatic.com/firebasejs/5.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.0/firebase-auth.js"></script>

<script src='https://cdn.firebase.com/js/client/2.2.1/firebase.js'></script>
<script src="https://cdn.firebase.com/libs/firebaseui/2.5.1/firebaseui.js"></script>
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.5.1/firebaseui.css" />

<script>
    var config = {
        apiKey: "<API_KEY>",
        authDomain: "<PROJECT_ID>.firebaseapp.com",
        databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
        storageBucket: "<BUCKET>.appspot.com",
        messagingSenderId: "<SENDER_ID>",
    };
    firebase.initializeApp(config);

    var ui = new firebaseui.auth.AuthUI(firebase.auth());

    var uiConfig = {
        callbacks: {
            signInSuccessWithAuthResult: function(authResult, redirectUrl) {
                // User successfully signed in.
                // Return type determines whether we continue the redirect automatically
                // or whether we leave that to developer to handle.
                return true;
            },
            uiShown: function() {
                // The widget is rendered.
                // Hide the loader.
                document.getElementById('loader').style.display = 'none';
            }
        },
        // Will use popup for IDP Providers sign-in flow instead of the default, redirect.
        signInFlow: 'popup',
        signInSuccessUrl: 'http://example.com',// Sign in後の遷移先
        signInOptions: [
            // Leave the lines as is for the providers you want to offer your users.
            firebase.auth.GoogleAuthProvider.PROVIDER_ID,
            firebase.auth.FacebookAuthProvider.PROVIDER_ID,
            firebase.auth.TwitterAuthProvider.PROVIDER_ID,
            firebase.auth.GithubAuthProvider.PROVIDER_ID,
            firebase.auth.EmailAuthProvider.PROVIDER_ID,
            firebase.auth.PhoneAuthProvider.PROVIDER_ID
        ],
        // Terms of service url.
        tosUrl: ''
    };

    ui.start('#firebaseui-auth-container', uiConfig);

</script>
<h1>Welcome to My Awesome App</h1>
<div id="firebaseui-auth-container"></div>
<div id="loader">Loading...</div>

こんな感じで表示されます。 f:id:inari111:20180722220426p:plain

Sign in with Twitter をクリックした後 Authorize app すればログイン完了です。
Firebase Authenticationのコンソールを見るとユーザーが作成されていることがわかります。

f:id:inari111:20180722222559p:plain

まとめ

FirebaseUIを使うと簡単にTwitterログインを実装することができました。
次はFirebase SDKを使ってログイン機能の実装をしていきたいと思います。

BigQueryの集計結果をテーブルに保存した後GCSにエクスポートしてみた

こういう細かいネタもブログに残していこうと思います。
ってブログ書く度に決意しているんだけど続かないですね...

BigQueryの集計結果をテーブルに保存

使うpackageは cloud.google.com/go/bigquery です。このpackageはまだbetaらしいです。
google.golang.org/api/bigquery/v2 でもできるはずですが、cloud.google.com/go/bigqueryのほうが簡潔に書けそうな感じがしました。

コードはこんな感じ
AppEngine上で動かすコードになっていますが、一部変更すれば、AppEngine以外でも動くはずです。

package hoge

import (
    "context"

    "cloud.google.com/go/bigquery"
    "google.golang.org/appengine"
)

// BigQueryのSchema例
type schema struct {
    UserID   string
    UserName string
}

const datasetID = "test"
const tableID = "test"
const query = ""

// 関数名適当で申し訳ない
func bq(ctx context.Context) error {
    client, err := bigquery.NewClient(ctx, appengine.AppID(ctx))
    if err != nil {
        return err
    }

    // 集計結果を保存するDatasetを作成
    d := client.Dataset(datasetID)
    d.Create(ctx, &bigquery.DatasetMetadata{
        Location: "US", // お好みで
    })

    // 集計結果を保存するTable作成
    s, err := bigquery.InferSchema(schema{})
    if err != nil {
        return err
    }
    table := client.Dataset(datasetID).Table(tableID)
    if err := table.Create(ctx, &bigquery.TableMetadata{Schema: s}); err != nil {
        return err
    }

    // Queryの準備
    q := client.Query(query)
    q.WriteDisposition = bigquery.WriteTruncate // お好みで
    q.Dst = &bigquery.Table{
        ProjectID: appengine.AppID(ctx),
        DatasetID: datasetID,
        TableID:   tableID,
    }
    // 最大レスポンスサイズを超えそうだったらtrueにする
    // see: https://cloud.google.com/bigquery/querying-data#large-results
    q.AllowLargeResults = true

    job, err := q.Run(ctx)
    if err != nil {
        return err
    }
    jobStatus, err := job.Wait(ctx)
    if err != nil {
        return err
    }
    if err := jobStatus.Err(); err != nil {
        return err
    }
    return nil
}

BigQueryのテーブル指定してをGCSにexport

CSVファイルにしてexportしてみます

package hoge

import (
    "cloud.google.com/go/bigquery"
    "google.golang.org/appengine"
)

const datasetID = "test"
const tableID = "test"
// export先を指定
const gcsURI = "" // TODO

func export(ctx context.Context) error {
    client, err := bigquery.NewClient(ctx, appengine.AppID(ctx))
    if err != nil {
        return err
    }

    gcsRef := bigquery.NewGCSReference(gcsURI)
    gcsRef.SourceFormat = bigquery.CSV // defaultはCSV
    gcsRef.FieldDelimiter = ","        //defaultは ","
    gcsRef.AllowQuotedNewlines = true

    extractor := client.Dataset(datasetID).Table(tableID).ExtractorTo(gcsRef)
    extractor.DisableHeader = false // お好みで
    job, err := extractor.Run(ctx)
    if err != nil {
        return err
    }
    status, err := job.Wait(ctx)
    if err != nil {
        return err
    }
    if status.Err(); err != nil {
        return err
    }
    return nil
}

まとめ

  • cloud.google.com/go/bigquery を使って集計結果の保存とGCSへのexportをやってみました
  • bigquery packageを初めて使ったので調査のコストは少しありました
  • UTF-8でexportされるのでExcelで開くと文字化けするかも...

Goのstructを比較してdiffを見るなら godebug/pretty が便利

Goのテストでstructを比較するときに reflect.DeepEqual で比較することがあります。

テストを書き、テストを実行するとFAILだったとき...つらい...
structが大きいとさらにつらい。どのフィールドの値が違うのかわかると早くテストを直せて(または実装を直せて)嬉しいですよね。

そんなときgodebug/prettyを使うとdiffを見れて便利です。

github.com

こんなUserのstructがあったとします

type User struct {
    ID string
    Name string
    Email string
}

テストでstructを reflect.DeepEqual で比較し一致するかをチェックします
一致しなかった場合、pretty.Compare でdiffを表示してみます

func TestCompare(t *testing.T) {
    tests := []struct{
        user User
        want User
    } {
        {
            user: User{
                ID: "id",
                Name: "inari",
                Email: "inari111@example.com",
            },
            want: User{
                ID: "id",
                Name: "inari111",
                Email: "inari111@example.com",
            },
        },
        {
            user: User{
                ID: "id",
                Name: "inari111",
                Email: "inar222@example.com",
            },
            want: User{
                ID: "id",
                Name: "inari111",
                Email: "inari111@example.com",
            },
        },
    }

    for _, test := range tests {
        if !reflect.DeepEqual(test.user, test.want) {
            t.Errorf("error: %s", pretty.Compare(test.user, test.want))
        }
    }
}

テスト実行結果

$ go test
--- FAIL: TestCompare (0.00s)
    user_test.go:49: error:  {
          ID: "id",
        - Name: "inari",
        + Name: "inari111",
          Email: "inari111@example.com",
         }
    user_test.go:49: error:  {
          ID: "id",
          Name: "inari111",
        - Email: "inar222@example.com",
        + Email: "inari111@example.com",
         }
FAIL
exit status 1
FAIL    github.com/inari111/godebug-pretty  0.011s

小さいstructなら必要ありませんが、大きいstructの場合は助かりますね。

Goでsliceを結合するときのパフォーマンスについて

sliceを結合するときに、どういう書き方をするのがパフォーマンスがいいのか気になって検証してみた。 検証したのは2パターン。

パターン1: slice同士を結合

func appendSlice(n int) []string {
    sliceA := make([]string, n)
    sliceB := make([]string, n)

    for i := 1; i <= n; i++ {
        sliceA = append(sliceA, "a")
        sliceB = append(sliceB, "b")
    }
    sliceA = append(sliceA, sliceB...)

    return sliceA
}

パターン2: forで回しながらappend

func appendSlice2(n int) []string {
    sliceA := make([]string, n)
    sliceB := make([]string, n)

    for _, s := range sliceB {
        sliceA = append(sliceA, s)
    }

    return sliceA
}

パフォーマンスを計測するためにベンチマークを書く。

package main

import "testing"

func BenchmarkAppendSlice(b *testing.B) {
    b.ResetTimer()
    appendSlice(b.N)
}

func BenchmarkAppendSlice2(b *testing.B) {
    b.ResetTimer()
    appendSlice2(b.N)
}

パターン1のベンチマーク

$ go test -count 10 -test.bench BenchmarkAppendSlice

BenchmarkAppendSlice-8       2000000           631 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
BenchmarkAppendSlice-8       2000000           631 ns/op
BenchmarkAppendSlice-8       2000000           595 ns/op
BenchmarkAppendSlice-8       2000000           602 ns/op
BenchmarkAppendSlice-8       2000000           600 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
BenchmarkAppendSlice-8       2000000           598 ns/op
BenchmarkAppendSlice-8       2000000           597 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
PASS
ok      github.com/inari111/test    18.915s

パターン2のベンチマーク

$ go test -count 10 -test.bench BenchmarkAppendSlice2
BenchmarkAppendSlice2-8      5000000           253 ns/op
BenchmarkAppendSlice2-8     10000000           243 ns/op
BenchmarkAppendSlice2-8     10000000           206 ns/op
BenchmarkAppendSlice2-8     10000000           204 ns/op
BenchmarkAppendSlice2-8     10000000           198 ns/op
BenchmarkAppendSlice2-8     10000000           205 ns/op
BenchmarkAppendSlice2-8     10000000           200 ns/op
BenchmarkAppendSlice2-8     10000000           201 ns/op
BenchmarkAppendSlice2-8     10000000           201 ns/op
BenchmarkAppendSlice2-8     10000000           202 ns/op
PASS
ok      github.com/inari111/test    22.293s

まとめ

パターン1のslice同士の結合のほうがパフォーマンスよさそう。
もし間違ってたら教えていただけると嬉しい。

fish shellでgoenvを使う

メモ書きとして残す。

$ brew install goenv

config.fishを編集する まだ書き方に慣れない

eval (goenv init - | source)
set -x PATH $HOME/.goenv/bin $PATH
set -gx PATH ‘/Users/inari111/.goenv/shims’ $PATH
set -gx GOENV_SHELL fish
goenv install 1.9.2
goenv global 1.9.2

datastoreに依存したテストはStronglyConsistentDatastoreをtrueにする

Goを書き始めた頃、テストで下記のようにcontextを生成していた。

ctx, done, e := aetest.NewContext()
if e != nil {
    t.Fatal(e)
}
defer done()

// 1. Putする

// 2. 1でPutしたデータをGetする

このコードには、datastoreにPutした直後にGetするとデータを取得できずテストが落ちるという問題があった。
これを解決するには aetest.Options{StronglyConsistentDatastore: true} Instance生成時の引数に渡してあげればいい。
しかし、aetest.NewContext には引数でoptionを渡すことはできない。 github.com

aetest.NewInstance にoptionを渡せるメソッドを作り、datastoreが絡むテストはこのメソッドを使うようにしている。

func NewContext(option aetest.Options) (context.Context, func(), error) {
    inst, err := aetest.NewInstance(&option)
    if err != nil {
        return nil, nil, err
    }
    req, err := inst.NewRequest("GET", "/", nil)
    if err != nil {
        inst.Close()
        return nil, nil, err
    }
    ctx := appengine.NewContext(req)
    return ctx, func() {
        inst.Close()
    }, nil
}

呼び出し側

option := aetest.Options{StronglyConsistentDatastore: true}
ctx, done, e := NewContext(option)
if e != nil {
    t.Fatal(e)
}
defer done()

このようにすれば、Putしてdatastoreに反映したものをGetできるようになる。

ブログに残しておこうと思っていたのにいつの間にか数ヶ月経っていてよくないな。。

hatebu: はてブのホットエントリーを表示するCLIツールを作った

最近Goを書いているので、勉強がてらCLIツールを作ってみた。

作ったもの

はてブのホットエントリー(テクノロジー)の一覧を表示するCLIツール hatebu を作った。

github.com

学生の頃からはてブが好きで、2014年頃はホットエントリーを収集して過去のホットエントリーをまとめて見れるようなサービスを作ったりしていた。
CLIツールを作りにあたり、普段よく使うものがよかったので、ホットエントリーを表示するものにした。 今はテクノロジーしか取得していないが、将来的に他のもサブコマンドで指定できるようにしたい。

今回、CLIパッケージは spf13/cobra を使ってみた。元Dockerの中の人で、今はGoogleで働いているらしい。

github.com

サブコマンドは個別にファイル作れば追加できるので拡張性がよさそう。

ホットエントリー一覧を表示するCLIツールは既に作っている人がいて、実装する上で大変参考にさせていただきました。ありがとうございます。
github.com