GitHubから草(Contribution数)🌿を取得して、本ブログアプリに表示しました!
このブログアプリに装飾を増やしたいと思い、GraphQLの素振りも兼ねてGitHubのAPIからContribution数を取得して表示するようにしました。
以下が実装の手引になります!
①GitHubのAPIの仕様を確認する。
②サーバーサイドでGitHubAPIv4からContirbution数を取得する。
③フロントエンドからサーバーサイドにリクエストし、結果を描画する。
①GitHubのAPI仕様を確認する。
GitHubAPI v4 がGraphQLで公開されているため、サーバーサイドでGolangでContribution数を取得します。
プログラム作成に必要な材料としては以下になります。
- GitHubのPersonalAccessToken
- GraphQLクライアント(必要であれば)
- API仕様
PersonalAccessTokenの取得
GitHubのコンソールから、
自分のプロフィールアイコンクリック > settings > 左メニューの一番下Developer settings
> Personal Access Token
になります。
権限については、デフォルト状態でPublic Repositories (read-only)
にチェックがついていればContribution数取得には問題ありません!
GraphQLクライアント
GraphQLの基本としては、POSTリクエストでBodyにQuery(クエリ)とVariables(引数)を指定することなります。
以下はGitHubAPI v4へのリクエストのサンプルです。単純なQueryを渡しています。
TOKENの部分はPersonalAccessTokenになります。
curl -H "Authorization: bearer TOKEN" -X POST -d " \\
{ \\
\\"query\\": \\"query { viewer { login }}\\" \\
} \\
" https://api.github.com/graphql
リクエストすると、GraphQLサーバーが以下のように、リクエストしたクエリと同じ構造でJSONでデータを返してきます。
{"data":{"viewer":{"login":"shoet"}}}
このときデータを受け取る側では、JSONをデシリアライズしてアプリケーション上で使用できるようにする必要がありますが、Goでこれを行うには、あらかじめ構造体を定義してUnmarshalする必要があります。
構造がシンプルなデータであれば気にする必要がありませんが、ネストの深いデータでは定義する構造体が多く大変になってしまいます。
そこで今回はshurcooL/graphqlというGoパッケージを活用します。
詳細な使い方は、パッケージのリポジトリや、後述するサンプルプログラムを参照ください。
このパッケージはSQLのクエリビルダのような役割をしてくれることと、JSONをデシリアライズする際の構造体定義を楽にしてくれます。
API仕様
GitHubAPI v4の仕様はこちらから確認できます。
https://docs.github.com/ja/graphql
有り難いことに公式からクエリのWebクライアントが提供されています。
https://docs.github.com/ja/graphql/overview/explorer
こちらを活用することで、プログラムするまえにクエリを試すことができます。
使用するにはGitHubアカウントでのSignInが必要になります。
また、コンソール左側の本のマークからデータ構造を検索することができます。
こちらのおかげで、ドキュメントをあちこち参照する必要が無くなり、開発効率によいです!
完成したプログラムがこちらになります。
GetContributions()でユーザー名、取得開始日、取得終了日を入力すると週刻みで日毎のContribution数と、草のカラーコードを返してきます。
- サーバーサイドでのContribution数を取得
import (
"context"
"fmt"
"net/url"
"time"
"github.com/shurcooL/graphql"
"golang.org/x/oauth2"
)
type GitHubV4APIClient struct {
githubPersonalAccessToken string
}
func NewGitHubV4APIClient(
githubPersonalAccessToken string,
) *GitHubV4APIClient {
return &GitHubV4APIClient{
githubPersonalAccessToken: githubPersonalAccessToken,
}
}
type GitHubContributionWeeks []struct {
ContributionDays []struct {
Date string `json:"date"`
Color string `json:"color"`
ContributionCount int `json:"contributionCount"`
} `json:"contributionDays"`
}
func (g *GitHubV4APIClient) GetContributions(ctx context.Context, username string, fromDateUTC time.Time, toDateUTC time.Time) (GitHubContributionWeeks, error) {
apiUrl, err := url.Parse("https://api.github.com/graphql")
if err != nil {
return nil, fmt.Errorf("failed to parse url: %v", err)
}
var query struct {
User struct {
ContributionCollection struct {
ContributionCalendar struct {
Weeks GitHubContributionWeeks `json:"weeks"`
}
} `graphql:"contributionsCollection(from: $from, to: $to)"`
} `graphql:"user(login: $login)"`
}
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: g.githubPersonalAccessToken, TokenType: "Bearer"},
)
httpClient := oauth2.NewClient(context.Background(), src)
client := graphql.NewClient(apiUrl.String(), httpClient)
type DateTime struct{ time.Time }
variables := map[string]interface{}{
"login": graphql.String(username),
"from": DateTime{fromDateUTC},
"to": DateTime{toDateUTC},
}
if err := client.Query(context.Background(), &query, variables); err != nil {
return nil, fmt.Errorf("failed to query: %v", err)
}
return query.User.ContributionCollection.ContributionCalendar.Weeks, nil
}
フロントエンドでの描画は下記のコードになります。
①ContributionTile > ②ContributionColumn > ③GitHubContributionsGrid
①ContributionTile: 草1マスを表現
②ContributionColumn: 草の縦1列を表現(1週間分)
③GitHubContributionsGrid: ContributionColumnを横方向に並べる
- フロントエンドで草を描画
import { GitHubContributions } from '@/types/api'
import styled from 'styled-components'
type GitHubContributionsProps = {
contributions: GitHubContributions[]
}
const ContributionTile = styled.div<{ color: string }>`
width: 0.7rem;
height: 0.7rem;
${({ color }) => `background-color: ${color};`}
border: none;
border-radius: 20%;
`
const ContributionColumn = (props: { contribution: GitHubContributions }) => {
const { contribution } = props
const Column = styled.div`
display: flex;
flex-direction: column;
`
return (
<Column>
{contribution.contributionDays.map((cd, idx) => {
return (
<div
style={{
paddingBottom:
contribution.contributionDays.length - 1 === idx
? '0'
: '0.1rem',
}}
>
<ContributionTile color={cd.color}></ContributionTile>
</div>
)
})}
</Column>
)
}
export const GitHubContributionsGrid = (props: GitHubContributionsProps) => {
const { contributions } = props
const Row = styled.div`
&::-webkit-scrollbar{
display: none;
}
display: flex;
flex-direction: row;
justify-content: end;
overflow-x: scroll;
padding: 0 0.5rem;
`
return (
<>
<a href="https://github.com/shoet" target="_black">
<Row style={{ backgroundColor: '' }}>
{contributions.map((c, idx) => {
return (
<div
style={{
marginLeft: 0 === idx ? '0' : '0.1rem',
}}
>
<ContributionColumn contribution={c} />
</div>
)
})}
</Row>
</a>
</>
)
}
ソース全体は、このブログのソースコードになります。
初めてGraphQLを触った所感としては、
利用する側としては、ネストされた関係にあるデータの取得はしやすいように感じました。
たとえばユーザーのデータを取得してしまえば、その配下にあるユーザープロフィールのようなデータは同時に取得するといったケースです。
今度は、サーバー提供するケースを素振りしてみたいと思います!