GitHubのAPIを使って、リモートレポジトリの一覧を得る 2. httpsでのgetとJSON処理
前記事でcurlとjqでそれなりに必要な情報は取れているので、いよいよC++ Builder CEでIndyを使ってまずJSONなレスポンスを得ます。
新規のVCLプログラムを用意して、まっさらのフォームに、下図のようにTListView,TIdHTTP,TIdSSLIOHandlerSocketさらにお好みでTIdLogDebugを置きます。レイアウトは暫定でおけです。後でファインチューニングします。

まずTIdHTTPを使ってcurlがやっていた
curl --header "Authorization: bearer ghp_use_your_own_pat_here" "https://api.github.com/user/repos"
これを実現します。フォームに置いたTIdHTTPのインスタンスIdHTTP1のプロパティーは、

このようにすれば大抵のAPIでGetならば大丈夫です。IdLogDebug1とかIdSSLIOHanderSocketOpenSSL1とかとリンクしているのに注意してください。IdLogDebug1はオプションです。PATの情報はヘッダーに入れ込みますから、以下のようにいきなりコンストラクターでスタートしましょうかね?
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
ListRepositories("https://api.github.com/user/repos","Authorization: bearer ghp_I_Dont_show_actural_personal_access_token");
}
関数ListRepositoriesの中身は、
void ListRepositories(AnsiString baseurl,AnsiString pat)
{
TMemoryStream* ms = new TMemoryStream(); //取得したデータを格納する
TStringList* atlist = new TStringList();
TIdHTTP* kick = Form1->IdHTTP1;
int ret = 0;
kick->Request->CustomHeaders->Clear(); // for re-use
kick->Request->CustomHeaders->FoldLines = false;
kick->Request->CustomHeaders->Add(pat);
try {
kick->Get(baseurl,ms);
}
catch(const Exception& e){
ShowMessage(e.ClassName());
return;
}
ms->Position = 0;
atlist->LoadFromStream(ms, TEncoding::UTF8);
jsonorig="";
for( int i = 0 ; i < atlist->Count ; i++ )
jsonorig += atlist->Strings[i];
LStringReader = new TStringReader(jsonorig);
LJsonTextReader = new TJsonTextReader(LStringReader);
LIterator = new TJSONIterator(LJsonTextReader);
int i = 0;
Form1->Memo1->Lines->Clear();
Form1->IdHTTP1->Disconnect();
Form1->ListView1->Clear();
while( DumpEntry(i++ ) )
;
// sorting vector using date comparison
std::sort(entries.begin(),entries.end(), datecomp);
// display data in tlistview
for( auto itr = entries.begin() ; itr != entries.end() ; ++itr ) {
TListItem* ListItem = Form1->ListView1->Items->Add();
ListItem->Caption = itr->name;
ListItem->SubItems->Add(itr->description);
ListItem->SubItems->Add(itr->updated);
}
}
中盤でString jsonorigにレスポンスをまるごと読み込みます。ここまでの時点で、

とかのエラーが出ましたけど、メッセージを読めばsslがらみの暗号化周辺のエラーが出ているというのが分かるので、TIdSSLIOHandlerSocketのプロパティーの、

で、オケです。ここまででDebug出力が、
デバッグ出力: Stat 接続しました。 プロセス Project1.exe (25208)
デバッグ出力: Sent 2025/05/08 13:59:14: GET /user/repos HTTP/1.1<EOL>Authorization: bearer ghp_ここも消さないとね<EOL>Host: api.github.com<EOL>Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8<EOL>User-Agent: Mozilla/3.0 (compatible; Indy Library)<EOL><EOL> プロセス Project1.exe (25208)
デバッグ出力: Recv 2025/05/08 13:59:14: HTTP/1.1 200 OK<EOL>Date: Thu, 08 May 2025 04:59:14 GMT<EOL>Content-Type: application/json; charset=utf-8<EOL>Content-Length: 21173<EOL>Cache-Control: private, max-age=60, s-maxage=60<EOL>Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With<EOL>ETag: "a2f5697272e929a102e8a0ec374ecbde5039d056d8425e7229528af8bfdf1b46"<EOL>X-OAuth-Scopes: delete_repo, repo, user<EOL>X-Accepted-OAuth-Scopes: <EOL>X-GitHub-Media-Type: unknown, github.v3<EOL>x-github-api-version-selected: 2022-11-28<EOL>X-RateLimit-Limit: 5000<EOL>X-RateLimit-Remaining: 4999<EOL>X-RateLimit-Reset: 1746683954<EOL>X-RateLimit-Used: 1<EOL>X-RateLimit-Resource: core<EOL>Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset<EOL>Acce プロセス Project1.exe (25208)
デバッグ出力: Recv 2025/05/08 13:59:14: :"bootnext","full_name":"alt-doc-nao/bootnext","private":false,"owner":{"login":"alt-doc-nao","id":189937208,"node_id":"U_kgDOC1I2OA","avatar_url":"https://avatars.githubusercontent.com/u/189937208?v=4","gravatar_id":"","url":"https://api.github.com/users/alt-doc-nao","html_url":"https://github.com/alt-doc-nao","followers_url":"https://api.github.com/users/alt-doc-nao/followers","following_url":"https://api.github.com/users/alt-doc-nao/following{/other_user}","gists_url":"https://api.github.com/users/alt-doc-nao/gists{/gist_id}","starred_url":"https://api.github.com/users/alt-doc-nao/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alt-doc-nao/subscriptions","organizations_url":"https://api.github.com/users/alt-doc-nao/orgs","repos_url":"https://api.github.com/users/alt-doc-nao/repos","events_url":"https://api.github.com/users/alt-doc-nao/events{/privacy}","received_events_url":"https://api.github.com/users/alt-doc-nao/received_events","type":"User","user_vi プロセス Project1.exe (25208)
等と得られていて、肝心のレスポンスコードが、HTTP/1.1 200 OKなら文字通り、おけです。後は、JSONボディーのスキャンができますね。ちなみに今回の設定では、

IdHTTP->Request->UserAgentが上図のようにMozilla/3.0(compatible; Indy Library)で通りましたけど、筆者の経験だとこれで文句言われることもあります。curlで動いて、Indyで動かない場合は、ここをチェックすると良いです。(滅多にないケースですが…..。)
さて、JSONの読み込みに進みますけれども、RadStudioつまりDelphiないしC++ BuilderでJSONの解析をする場合には、色々なやり方がありますが、今回のようなJSONに織り込まれているデータの構造や階層が単純な場合は、まず、ヘッダーで、
#include <DBXJSON.hpp>
#include <System.JSON.Readers.hpp>
#include <System.JSON.Builders.hpp>
としておいて、グローバル変数としては、例えば
String jsonorig;
TStringReader* LStringReader;
TJsonTextReader* LJsonTextReader;
TJSONIterator* LIterator;
と宣言しておいて、
LStringReader = new TStringReader(jsonorig);
LJsonTextReader = new TJsonTextReader(LStringReader);
LIterator = new TJSONIterator(LJsonTextReader);
として使用準備します。TStringReader,TJsonTextReader,TJSONIteratorの関係に注意してください。最後のLIteratorを使います。具体的には、bool DumpEntry(int index)なる関数を示しましょう。
bool DumpEntry(int index)
{
String aname,adescription,adate,ais_private,aurl;
String aid,apath;
String aemptiness;
String aclone_url = "clone_url";
ais_private = "private";
adate = "updated_at";
adescription = "description";
aurl = "html_url";
aemptiness = "size";
String prefix = Format("[%d].",ARRAYOFCONST((index)));
aname = prefix + "name";
adescription = prefix + "description";
adate = prefix + adate;
aclone_url = prefix + aurl;
aid = prefix + "id";
apath = prefix + "path";
aemptiness = prefix + "size";
ais_private = prefix + ais_private;
struct staentry aentry;
if( LIterator->Find(aname) ){
Form1->Memo1->Lines->Add("");
Form1->Memo1->Lines->Add(IntToStr(index+1) + "----------------------------------------");
Form1->Memo1->Lines->Add("name: " + LIterator->AsString);
aentry.name = LIterator->AsString;
}
else
return false;
if( LIterator->Find(aemptiness) ){
int size;
bool flag;
String results;
size = LIterator->AsInteger;
if( size == 0 )
flag = true;
else
flag = false;
if( flag )
results = "true";
else
results = "false";
Form1->Label5->Caption = IntToStr(size);
Form1->Memo1->Lines->Add("empty: " + results);
if( flag )
aentry.empty = &bempty;
else
aentry.empty = &bnotempty;
}
if( LIterator->Find(aclone_url)){
Form1->Memo1->Lines->Add("clone_url: " + LIterator->AsString);
//aentry->SubItems->Add(LIterator->AsString);// #2 clone_url
aentry.url = LIterator->AsString;
}
else
return false;
if( LIterator->Find(adescription)){
Form1->Memo1->Lines->Add("description: " + LIterator->AsString);
//aentry->SubItems->Add(LIterator->AsString);
aentry.description = LIterator->AsString;
}
else
return false;
if( LIterator->Find(apath)){
Form1->Memo1->Lines->Add("path: " + LIterator->AsString);
//aentry->SubItems->Add(LIterator->AsString);
}
if( LIterator->Find(aid) ){
Form1->Memo1->Lines->Add("id: " + IntToStr(LIterator->AsInteger));
//aentry->SubItems->Add(IntToStr(LIterator->AsInteger));
}
if( LIterator->Find(adate)) {
Form1->Memo1->Lines->Add("date: " + LIterator->AsString);
//aentry->SubItems->Add(LIterator->AsString);
aentry.updated = ISO8601ToDate(LIterator->AsString,false);
}
else
return false;
if( LIterator->Find(ais_private)){
Form1->Memo1->Lines->Add("private: " + LIterator->AsBoolean);
if( LIterator->AsBoolean )
aentry.stprivate = true;
else
aentry.stprivate = false;
//aentry.empty = LIterator->AsString;
}
else
return false;
if( LIterator->Find(aclone_url))
Form1->Memo1->Lines->Add("clone_url: " + LIterator->AsString);
else
return false;
entries.push_back(aentry);
Form1->Label3->Caption = IntToStr(index+1);
return true;
}
基本的には、LIterator->Find(entry)がtrueだったとき、そのentry=keyに相当するvalueが、文字列ならば、LIterator->AsStringで得られます。真偽値であるならば、LIterator->AsBooleanで得られます。整数値の場合は、LIterator->AsIntegerですね。関数の冒頭を見ると、
String prefix = Format("[%d].",ARRAYOFCONST((index)));
aname = prefix + "name";
adescription = prefix + "description";
としていますので、prefixは最初のエントリー(index=0)の各要素は、
“[0].name”や”[0].description”でFindできます。いわば絶対アドレッシングですね。いまIteratorがどこを指しているかを気にすることなく、要素を抜き出すことができますから、簡単に処理ができます。curlとjqの時に使った、
$ curl -s --header "Authorization: bearer ghp_invalid_access_token_use_your_own_token_here" "https://api.github.com/user/repos" | jq -r '.[] | .name, .private, .description, .html_url, .updated_at'
と同じやり方だということがわかりますでしょうか?jqにおいても、.[]は配列のスキャンです。
さて見つかったentryを構造体に入れて、そのままvectorに入れています。それからvectorのソートを行いますが、updated_atな日時で降順(新しいものが上)にソートしたいので、
std::sort(entries.begin(),entries.end(), datecomp);
における比較関数は、
struct staentry {
String name;
bool stprivate;
String description;
String url;
TDateTime updated;
bool empty;
};
bool datecomp( const struct staentry &a, const struct staentry &b )
{
return( a.updated > b.updated );
}
になります。TListViewの構成や最終結果の吟味は次の記事で…..。
コメント