C++ Builder CE 中級プログラミング

GitHubAPIを使って、リモートレポジトリの一覧を得る 2. httpsでのgetとJSON処理

前記事でcurljqでそれなりに必要な情報は取れているので、いよいよ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の構成や最終結果の吟味は次の記事で…..。

コメント