【Delphi】REST クライアントライブラリを使う

# はじめに
Delphi は XE5 (2013 年) において **REST** (Representational State Transfer) クライアントライブラリが実装されたのですが、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/c0d58087-bc15-feb0-464f-96bf20b51f01.png)
`DataSnap` という文字列を見て **「あぁ、Professional Edition では使えないのか」** と勝手に思い込んでいました。

 - [Representational State Transfer (Wikipedia)](https://ja.wikipedia.org/wiki/Representational_State_Transfer)
 - [REST クライアントライブラリ (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/REST_%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88_%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/5ba53b91-1a99-47b0-048d-92e9b95638f6.png)
**そんなことはなかったぜ！** (今更感)

# REST クライアントライブラリの使い方

概要は米澤さんの記事をどうぞ。

 - [Delphi Community Edition Meet up ! - REST 編 (Qiita: @CYonezawa)](https://qiita.com/CYonezawa/items/e20c3018c6eb6e1200a0)


## Qiita の記事を取得するコードの生成

Qiita には REST API が用意されていますので、ライブラリの使用例として、私が書いた Qiita の記事を取得してみます。

使用する Qiita の API は `GET /api/v2/users/:user_id/items` です。記事一覧を取得する API になります。`:user_id` はプレースホルダーで、実際には Qiita のユーザーID が入ります。

 - [GET /api/v2/users/:user_id/items (Qiita Developer)](https://qiita.com/api/v2/docs#get-apiv2usersuser_iditems)


REST クライアントライブラリのドキュメントが若干分かりにくいので、まずは **REST Debugger** を使ってみます。Delphi IDE の `[ツール | REST デバッガ] ` または、`$(BDS)\BIN\` にある **RESTDebugger.exe** を直接起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/811f85fc-75ac-8388-3313-8fa5cbfc4ad6.png)

### ・[要求] タブ
次のように指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/d732b8ae-3269-676a-5877-4e1dd1a08ce3.png)

| 項目 | 値 |
|:---|:---|
| メソッド | GET |
| URL | https://qiita.com |
| コンテンツタイプ | application/json |

### ・[パラメータ] タブ
次のように指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/e4f1c7ae-a22a-981f-4664-4f5122dcc85e.png)

| 項目 | 値 | 備考 |
|:---|:---|:---|
| リソース | /api/v2/users/ht_deko/items | ht_deko は私の Qiita ユーザーID|
| 要求パラメータ | [GET/POST /E] page=1<br>[GET/POST /E] per_page=10| とりあえず 10 件取得してみる設定|

要求パラメータは [追加] ボタンを押して追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/bcef7263-f662-3845-c29d-342d3adc1fcf.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/ed6a38c3-f556-6221-778f-cb5948242042.png)

### ・実行
`[要求の送信]` ボタンを押すと応答が返ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/93fd5a6f-9502-34b5-0d7e-7f701535a728.png)
`[コンポーネントのコピー]` ボタンを押すとこの接続のコンポーネントがクリップボードにコピーされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/1b218c7b-6265-336e-d80c-20575662c9a2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/f853f984-cdf4-4da4-3db3-1b85a2f7a8fa.png)


```pascal
object RESTClient1: TRESTClient
  BaseURL = 'https://qiita.com'
  Params = <>
end
object RESTRequest1: TRESTRequest
  AssignedValues = [rvConnectTimeout, rvReadTimeout]
  Client = RESTClient1
  Params = <
    item
      Name = 'page'
      Value = '1'
    end
    item
      Name = 'per_page'
      Value = '10'
    end>
  Resource = 'api/v2/users/ht_deko/items'
  Response = RESTResponse1
end
object RESTResponse1: TRESTResponse
end
```

普通はこれを VCL フォームや FMX フォームに貼り付けて使う事になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/295c4a61-8ef2-5276-4475-779d567ee74b.png)
**See also:**

 - [［REST デバッガ］の利用 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/%EF%BC%BBREST_%E3%83%87%E3%83%90%E3%83%83%E3%82%AC%EF%BC%BD%E3%81%AE%E5%88%A9%E7%94%A8)

## REST クライアントの最小限のコード

要は TRESTClient / TRESTRequest / TRESTResponse があればいいので、

```pascal
uses
  ..., REST.Types, REST.Client;

...

  var Request := TRESTRequest.Create(nil);
  try
    Request.Client := TRESTClient.Create(Request);
    Request.Response := TRESTResponse.Create(Request);
 
    { 要求 (リクエスト) に関するパラメータ等の処理 }

    ...

    Request.Execute;

    { 応答 (レスポンス) の処理 }

    ...

  finally
    Request.Free;
  end;
```

こんな感じのコードで REST API を扱える事になります。コンポーネントクラス (TComponent) の派生なので、TRESTRequest / TRESTResponse の破棄を TRESTRequest に任せる事ができます。

**See also:**

 - [REST.Client.TRESTClient (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/REST.Client.TRESTResponse)
 - [REST.Client.TRESTRequest (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/REST.Client.TRESTRequest)
 - [REST.Client.TRESTResponse (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/REST.Client.TRESTResponse)


## 具体的な使用例

以前、Qiita の記事をバックアップするコードを書いた事があります。

 - [Delphi で Qiita の記事をバックアップしてみる (Qiita)](https://qiita.com/ht_deko/items/f89774c5670952d52404)

記事内のコードを REST クライアントライブラリ対応に書き換えてみました。

```pascal:GetQiitaItems.dpr
program GetQiitaItems;

{$APPTYPE CONSOLE}
{$WARN GARBAGE OFF}

uses
  System.SysUtils, System.Classes, System.Rtti, System.IOUtils, System.RegularExpressions,
  System.Net.HttpClientComponent, REST.Client, REST.Types, System.JSON.Readers, System.JSON.Builders;

const
  EXP1 = 'https://qiita-image-store\.s3\..*amazonaws\.com/0/.+/(?<filename>.+\.(png|gif|jpg))';
  EXP2 = 'https://qiita\.com/%s/(items|private)/(?<filename>[0-9a-f]+)';
  PER_PAGE = 100;

type
  { TReplaceMethodClass }
  TReplaceMethodClass = class
    function NewPath(const AMatch: TMatch): String;
    function RelativePath(const AMatch: TMatch): String;
  end;

  function TReplaceMethodClass.NewPath(const AMatch: TMatch): String;
  begin
    result := './images/' + AMatch.Groups.Item['filename'].Value;
  end; { NewPath }

  function TReplaceMethodClass.RelativePath(const AMatch: TMatch): String;
  begin
    result := './' + AMatch.Groups.Item['filename'].Value + '.md';
  end; { RelativePath }

begin
  if ParamCount < 1 then
    begin
      Writeln('Usage:');
      Writeln(TPath.GetFileNameWithoutExtension(ParamStr(0)), ' <UserID>');
      Writeln;
      Writeln('UserID not specified.');
      Exit;
    end;

  // 格納先の作成
  var Dir := TPath.GetDirectoryName(ParamStr(0));
  var SrcDir := TPath.Combine(Dir, 'source');
  var ImgDir := TPath.Combine(SrcDir, 'images');
  TDirectory.CreateDirectory(ImgDir);

  // パラメータ
  var User_ID := ParamStr(1); // パラメータで与えられた文字列をユーザIDとして扱う
  var Page := 1;              // カレントページ

  var Request := TRESTRequest.Create(nil);
  var FileRequest := TNetHTTPRequest.Create(nil);
  var ResponseData := TMemoryStream.Create;
  var Body := TStringList.Create;
  var ReplaceMethod := TReplaceMethodClass.Create;
  try
    Request.Method := rmGET;
    Request.Client := TRESTClient.Create(Request);
    Request.Client.BaseURL := 'https://qiita.com';
    Request.Client.ContentType := 'application/json';
    Request.Response := TRESTResponse.Create(Request);
    Request.Resource := '/api/v2/users/{:user_id}/items';
    FileRequest.Client := TNetHTTPClient.Create(FileRequest);
    while True do
      begin
        // GET /api/v2/users/:user_id/items
        // https://qiita.com/api/v2/docs#get-apiv2usersuser_iditems
        Request.Params.Clear;
        Request.Params.AddItem(':user_id' , User_ID          , TRESTRequestParameterKind.pkURLSEGMENT);
        Request.Params.AddItem('page'     , Page.ToString    );
        Request.Params.AddItem('per_page' , PER_PAGE.ToString);
        Request.Execute;

        if (Request.Response.StatusCode <> 200) or      // HTTP 応答ステータスが 200 OK 以外 または
           (Request.Response.Content.Length < 100) then // 100 文字以下の応答 はエラーとする
          Break;

        // JSON データの読み込み
        var Iterator := TJSONIterator.Create(Request.Response.JSONReader);
        try
          Iterator.Recurse;
          while Iterator.Next do
            begin
              // データの取得
              Iterator.Recurse;
              (* BODY *)
              Iterator.Next('body');
              Body.Text := Iterator.AsString;
              (* ID *)
              Iterator.Next('id');
              var Id := Iterator.AsString;
              (* TAGS *)
              Iterator.Next('tags');
              var Tags := '';
              Iterator.Recurse;
              while Iterator.Next do
                begin
                  Iterator.Recurse;
                  Iterator.Next('name');
                  Tags := Tags + Iterator.AsString + ' ';
                  Iterator.Return;
                end;
              Iterator.Return;
              (* TITLE *)
              Iterator.Next('title');
              var Title := Iterator.AsString;
              (* URL *)
              Iterator.Next('url');
              var Url := Iterator.AsString;

              // TITLE と URL と Tags を標準出力
              Writeln('Title: ', Title    );
              Writeln('URL: '  , Url      );
              Writeln('Tags: ' , Tags.Trim);

              // 画像ファイルの取得
              for var Match in TRegEx.Matches(Body.Text, EXP1) do
                begin
                  var ImageURL := Match.Groups.Item[0].Value;
                  // 画像 URL を標準出力
                  Writeln(' - ', ImageURL);
                  // 画像ファイルの取得と保存
                  ResponseData.Clear;
                  FileRequest.Get(ImageURL, ResponseData);
                  ResponseData.SaveToFile(TPath.Combine(ImgDir, TPath.GetFileName(ImageURL)));
                end;

//              // Qiita 互換のヘッダ (任意)
//              Body.WriteBOM := False; // BOM なし
//              Body.LineBreak := #$0A; // LF 改行
//              var Header := '';
//              Header := Header + '---'                 + Body.LineBreak;
//              Header := Header + 'title: ' + Title     + Body.LineBreak;
//              Header := Header + 'tags: '  + Tags.Trim + Body.LineBreak;
//              Header := Header + 'author: '+ User_ID   + Body.LineBreak;
//              Header := Header + 'slide: ' + 'false'   + Body.LineBreak;
//              Header := Header + '---'                 + Body.LineBreak;
//              Body.Text := Header + Body.Text;

              // 画像ファイルのパス変更 (任意)
              Body.Text := TRegEx.Replace(Body.Text, EXP1, ReplaceMethod.NewPath);

              // 自身の投稿を相対パスへ (任意)
              Body.Text := TRegEx.Replace(Body.Text, Format(EXP2, [User_ID]), ReplaceMethod.RelativePath);

              // Markdown ファイルを出力
              Body.SaveToFile(TPath.Combine(SrcDir, Id + '.md'), TEncoding.UTF8);

              Writeln;
              Iterator.Return;
            end;
        finally
          Iterator.Free;
        end;
        Inc(Page); // 次のページへ
      end;
  finally
    ReplaceMethod.Free;
    Body.Free;
    ResponseData.Free;
    FileRequest.Free;
    Request.Free;
  end;
end. { Main }
```

### アクセストークンを使った記事の取得
アクセストークンを使った記事の取得に使用する Qiita の API は `GET api/v2/authenticated_user/items` です。

 - [GET /api/v2/authenticated_user/items (Qiita Developer)](https://qiita.com/api/v2/docs#get-apiv2authenticated_useritems)

先述のコードを次のように書き換えます。

```pascal:GetQiitaItems.dpr
  ...

  try
    Request.Method := rmGET;
    Request.Client := TRESTClient.Create(Request);
    Request.Client.BaseURL := 'https://qiita.com';
    Request.Client.ContentType := 'application/json';
    Request.AddAuthParameter('Authorization', 'Bearer ' + '[ここにアクセストークン]', TRESTRequestParameterKind.pkHTTPHEADER, [TRESTRequestParameterOption.poDoNotEncode]); // 追加
    Request.Response := TRESTResponse.Create(Request);
//  Request.Resource := '/api/v2/users/{:user_id}/items';   // 変更
    Request.Resource := '/api/v2/authenticated_user/items'; // 変更
    FileRequest.Client := TNetHTTPClient.Create(FileRequest);
    while True do
      begin
        // GET /api/v2/authenticated_user/items
        // https://qiita.com/api/v2/docs#get-apiv2authenticated_useritems
        Request.Params.Clear;
//      Request.Params.AddItem(':user_id' , User_ID          , TRESTRequestParameterKind.pkURLSEGMENT); // 削除
        Request.Params.AddItem('page'     , Page.ToString    );
        Request.Params.AddItem('per_page' , PER_PAGE.ToString);
        Request.Execute;
  ...
```

# おわりに
思い込みはよくないですね。

**REST Debugger** は Embarcadero 社のフリーツールとしても公開されており、RAD Studio / Delphi / C++ Builder をインストールしなくても使う事ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/3ee9b0b3-7b62-1c49-f6b0-0b98ac0c3733.png)

 - [REST Debugger (Embarcadero)](https://www.embarcadero.com/jp/free-tools/rest-debugger)

また、**Windows 10** (1803) 以降では `curl` が標準装備になっており、こちらを使って REST API のテストを行う事もできます。

```bat
curl "https://qiita.com/api/v2/authenticated_user/items?page=1&per_page=100" -H "Content-Type: application/json" -H "Authorization: Bearer [ここにアクセストークン]"
```

**See also:**

 - [Delphi で Rest クライアント (シンクソフト Delphi 開発ブログ)](http://thinksoft.sblo.jp/article/188184801.html)
 - [Delphi で Slack の投稿ボットを作る (日本語問題) (WorkToolSmith)](https://worktoolsmith.com/delphi-slack-start/)
 - [Delphi 10.2 Tokyo Enterprise と RAD Server で REST JSON な Web API を作る (Qiita: @kazinoue)](https://qiita.com/kazinoue/items/48cf6abd0076fbfe646b)
 - [REST+JSON はコンポーネントを使ってかんたんに取得★テーブル化 (Qiita: @kazaiso)](https://qiita.com/kazaiso/items/44bdcb9982530ab3830f)
 - [Delphi / C++Builder で REST API をカンタンに実装できる RAD Server で JSON を返す方法を３種類作ってみる (Qiita: @kazinoue)](https://qiita.com/kazinoue/items/56226634f242b1cc82c8)
 - [FDBatchMove を使って RESTで取得した JSON データをデータベースに書き込む (Qiita: @CYonezawa)](https://qiita.com/CYonezawa/items/acf9e9dbafe1d21b4946)
 - [Delphi 10 で Twilio api を叩いて SMS メッセージを送る (Qiita: @kenken2go)](https://qiita.com/kenken2go/items/86094dc2d4c2f5a4f4ed)
 - [cURL (Wikipedia)](https://ja.wikipedia.org/wiki/CURL)
 - [(RESTful) API でアプリをパワーアップする 20 の方法 (LearnDelphi.org)](https://learndelphi.org/ja/20-ways-to-supercharge-your-apps-with-apis/)
 - [Delphi で Apilayer REST API を使用する (blogs.embarcadero.com)](https://blogs.embarcadero.com/ja/using-apilayer-rest-apis-from-delphi-ja/)