libcurlでShotgrid REST APIを扱う

登場してから久しい**Shotgrid REST API**ですがほとんどの場合Pythonからの(JavascriptやC#もありそう)アクセスになるかと思います。
事実うちでもPythonからのアクセスです。
しかし最近、USDのAssetResolverを筆頭にC++から扱いたい場合が出てきそうだったのでどのようにアクセスすればいいのか調べてみることにしました。

## Shotgrid REST API

説明不要かと思いますがShotgridへアクセスする手段の一つであるREST APIです。
数年くらい前に登場しそれまでPythonを用いるしかなかったShotgridへのアクセスの新たな手段となりました。
うちの部署ではPython API版からこちらへ徐々にこちらへシフトしています。(といっても自分が作ったツールを改修しているだけだったりしますw)

[公式リファレンス](https://developers.shotgridsoftware.com/rest-api/ "ShotGrid REST API v1")

## 使用ライブラリ
### libcurl

言わずと知れたCURLのC言語ライブラリです。
**Shotgrid REST API**の公式リファレンスに`curl`コマンドを使ったサンプルが乗っていてかつライセンスが緩かったので採用しました。

[github](https://github.com/curl/curl "curl/curl")

### nlohmann json

**Shotgrid REST API**を扱うにあたってjsonをパース、ダンプする必要があるため評価が高いnlohmann jsonを使うことにしました。
nlohmann jsonとはヘッダーオンリーで扱えるModern C++用のjsonライブラリです。

[github](https://github.com/nlohmann/json "nlohmann/json")

## 環境構築

今回、環境構築が一番大変でした...
クライアント側のOSとしてWindowsとLinuxが使用されることが想定されるので*CMake*を用いてプロジェクトを作っていきます。
まず、gitのsub moduleで管理することを想定してプロジェクトディレクトリに`external`ディレクトリ作成しCURLとnlohmann jsonを`git clone`します。

```sh
project
├─CMakeLists.txt
│
└─external
    ├─curl
    └─json
```

`CMakeLists.txt`にそれぞれのライブラリをサブディレクトリとして登録し、CURLのビルドオプションを記述していきます。
今回はlibcurlを静的リンクしたかったのでそのためのオプションも追加しました。

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.20)

# C++バージョンの設定
set(CMAKE_CXX_STANDARD 20)

# ユニコードの設定
add_definitions(-DUNICODE -D_UNICODE)

# CURLのSSL設定オプション
set(CURL_USE_OPENSSL TRUE CACHE BOOL "")
mark_as_advanced(CURL_USE_OPENSSL)

# CURLのライブラリ設定オプション
set(BUILD_SHARED_LIBS FALSE CACHE BOOL "")
set(BUILD_STATIC_LIBS TRUE CACHE BOOL "")
mark_as_advanced(BUILD_SHARED_LIBS)
mark_as_advanced(BUILD_STATIC_LIBS)

# CURLのユニコード設定オプション
set(ENABLE_UNICODE TRUE CACHE BOOL "")
mark_as_advanced(ENABLE_UNICODE)

# サブディレクトリ追加
add_subdirectory(external/curl)
add_subdirectory(external/json)

project(SGRestAPI)


```

環境によってはこれで問題ないですがOpenSSLがインストールされていない場合エラーが出て止まってしまうため以下のように修正しました。

```sh
project
├─CMakeLists.txt
│
├─external
│  ├─curl
│  └─json
└─other
   └─curl-CMakeLists.txt
```

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.20)

# C++バージョンの設定
set(CMAKE_CXX_STANDARD 20)

# ユニコードの設定
add_definitions(-DUNICODE -D_UNICODE)

include(FetchContent)
FetchContent_Declare(
        libressl
        URL "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-3.8.2.tar.gz")

FetchContent_MakeAvailable(libressl)

list(APPEND CMAKE_PREFIX_PATH ${libressl_BINARY_DIR})

find_package(LibreSSL REQUIRED)

file(COPY_FILE "other/curl-CMakeLists.txt" "external/curl/CMakeLists.txt")

# CURLのSSL設定オプション
set(CURL_USE_OPENSSL TRUE CACHE BOOL "")
mark_as_advanced(CURL_USE_OPENSSL)

# CURLのライブラリ設定オプション
set(BUILD_SHARED_LIBS FALSE CACHE BOOL "")
set(BUILD_STATIC_LIBS TRUE CACHE BOOL "")
mark_as_advanced(BUILD_SHARED_LIBS)
mark_as_advanced(BUILD_STATIC_LIBS)

# CURLのユニコード設定オプション
set(ENABLE_UNICODE TRUE CACHE BOOL "")
mark_as_advanced(ENABLE_UNICODE)

# サブディレクトリ追加
add_subdirectory(external/curl)
add_subdirectory(external/json)

# libcurlのビルドにsslのビルドを依存させる
add_dependencies(libcurl_object ssl)

project(SGRestAPI)

add_executable(SGRestAPI main.cpp)

target_include_directories(SGRestAPI
    PUBLIC ${CMAKE_CURRENT_BINARY_DIR})

target_link_libraries(SGRestAPI
    PUBLIC CURL::libcurl
    nlohmann_json::nlohmann_json)
```

```cmake:curl-CMakeLists.txt(466行~492行)
if(CURL_USE_OPENSSL)
  find_package(LibreSSL REQUIRED)
  set(SSL_ENABLED ON)
  set(USE_OPENSSL ON)

  # Depend on OpenSSL via imported targets if supported by the running
  # version of CMake.  This allows our dependents to get our dependencies
  # transitively.
  if(NOT CMAKE_VERSION VERSION_LESS 3.4)
    list(APPEND CURL_LIBS LibreSSL::SSL LibreSSL::Crypto)
  else()
    list(APPEND CURL_LIBS ${LIBRESSL_LIBRARIES})
    include_directories(${LIBRESSL_INCLUDE_DIR})
  endif()

  if(CURL_DEFAULT_SSL_BACKEND AND CURL_DEFAULT_SSL_BACKEND STREQUAL "openssl")
    set(valid_default_ssl_backend TRUE)
  endif()

  set(CMAKE_REQUIRED_INCLUDES ${LIBRESSL_INCLUDE_DIR})
  if(NOT DEFINED HAVE_BORINGSSL)
    check_symbol_exists(OPENSSL_IS_BORINGSSL "openssl/base.h" HAVE_BORINGSSL)
  endif()
  if(NOT DEFINED HAVE_AWSLC)
    check_symbol_exists(OPENSSL_IS_AWSLC "openssl/base.h" HAVE_AWSLC)
  endif()
endif()
```

何をしているかというと、curlのサブディレクトリを追加する前にCMakeに対応しているOpenSSL派生であるLibreSSLを`FetchContent`したあとLibreSSLをパッケージとして利用できるように修正したcurlの`CMakeLists.txt`を上書きしています。
さらにビルドの依存関係が正しく来なかったので`add_dependencies`を使いlibcurlのビルドにsslのビルドを依存させました。
他にもっといい方法があると思うのですが自分のCMakeの知識が乏しくこのような力業で対処しました。
スマートな方法をご存知の方がいらっしゃればコメントで教えてくださると助かります。

## アクセストークンを取得
先ほども述べましたが**Shotgrid REST API**の[公式リファレンス](https://developers.shotgridsoftware.com/rest-api/ "ShotGrid REST API v1")に`curl`コマンドを用いたサンプルが乗っているのでこちらを参考に進めていきます。
当初、自分で`curl`コマンドをC++コードに書き換えようと思っていたのですがせっかくならこのコマンドをそのままコンバートできないかと思い調べたところ[`--libcurl`](https://everything.curl.dev/libcurl/libcurl)というオプションを見つけました。
こちらのオプション、名前の通りこのオプションとファイルパスを指定するとそちらに実行したコマンドの内容を`libcurl`を用いたコードに変えてくれるという代物で今回はこちらをがっつり使わせていただくことにしました。
お恥ずかしながらWindowsコマンドを一切書けないのでPythonを使ってバッチファイルの代わりにしています。
以下のコードは作成したShotgrid APIのスクリプト名とスクリプトキーから**Shotgrid REST API**のアクセストークンを取得しています。

```python:curl_authentication.py
import json
import subprocess

sg_url = '...'
sg_script_name = '...'
sg_script_key = '...'

curl_bin_dir = r'...'
out_cpp_file = r'...'

curl_command = f'{curl_bin_dir}/curl '
curl_command += f'-X POST '
curl_command += f'{sg_url}/api/v1/auth/access_token '
curl_command += f'-H "Content-Type: application/x-www-form-urlencoded" '
curl_command += f'-H "Accept: application/json" '
curl_command += f'-d "client_id={sg_script_name}&client_secret={sg_script_key}&grant_type=client_credentials" '
curl_command += f'--libcurl {out_cpp_file} '

res = subprocess.check_output(curl_command)
```

こちらを実行すると`out_cpp_file`に指定したファイルに以下のコードが出力されます。

```cpp:out_cpp_file.cpp
/********* Sample code generated by the curl command line tool **********
 * All curl_easy_setopt() options are documented at:
 * https://curl.se/libcurl/c/curl_easy_setopt.html
 ************************************************************************/
#include <curl/curl.h>

int main(int argc, char *argv[])
{
  CURLcode ret;
  CURL *hnd;
  struct curl_slist *slist1;

  slist1 = NULL;
  slist1 = curl_slist_append(slist1, "Content-Type: application/x-www-form-urlencoded");
  slist1 = curl_slist_append(slist1, "Accept: application/json");

  hnd = curl_easy_init();
  curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L);
  curl_easy_setopt(hnd, CURLOPT_URL, ".../api/v1/auth/access_token");
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, "client_id=...&client_secret=...&grant_type=client_credentials");
  curl_easy_setopt(hnd, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)98);
  curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1);
  curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/8.5.0");
  curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L);
  curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS);
  curl_easy_setopt(hnd, CURLOPT_CAINFO, "...");
  curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "POST");
  curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L);
  curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L);

  /* Here is a list of options the curl code used that cannot get generated
     as source easily. You may choose to either not use them or implement
     them yourself.

  CURLOPT_WRITEDATA was set to an object pointer
  CURLOPT_INTERLEAVEDATA was set to an object pointer
  CURLOPT_WRITEFUNCTION was set to a function pointer
  CURLOPT_READDATA was set to an object pointer
  CURLOPT_READFUNCTION was set to a function pointer
  CURLOPT_SEEKDATA was set to an object pointer
  CURLOPT_SEEKFUNCTION was set to a function pointer
  CURLOPT_ERRORBUFFER was set to an object pointer
  CURLOPT_STDERR was set to an object pointer
  CURLOPT_HEADERFUNCTION was set to a function pointer
  CURLOPT_HEADERDATA was set to an object pointer

  */

  ret = curl_easy_perform(hnd);

  curl_easy_cleanup(hnd);
  hnd = NULL;
  curl_slist_free_all(slist1);
  slist1 = NULL;

  return (int)ret;
}
/**** End of sample code ****/

```

こちらをもとに取得したデータをjsonにパースするコードを書きました。

```cpp:getting_acces_token.cpp
#include <format>
#include <memory>
#include <sstream>
#include <string>
#include <string_view>

#include <curl/curl.h>
#include <nlohmann/json.hpp>

// Shotgrid API関連の定数
static constexpr std::string_view SG_URL{"..."};
static constexpr std::string_view SG_SCRIPT_NAME{"..."};
static constexpr std::string_view SG_SCRIPT_KEY{"..."};

using json = nlohmann::json;

// 文字列リテラルを使用するため
using namespace std::literals::string_literals;
using namespace std::string_view_literals;

// CURL構造体のスマートポインタ
auto curl_deleter = [](CURL* _curl_ptr) { curl_easy_cleanup(_curl_ptr); };
using curl_ptr = std::unique_ptr<CURL, decltype(curl_deleter)>;

// stringstreamに出力するコールバック
auto writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) -> size_t
{
    if (!ptr)
    {
        return 0;
    }

    auto real_size = size * nmemb;
    auto* ss = reinterpret_cast<std::stringstream*>(userdata);

    ss->write(ptr, static_cast<std::streamsize>(real_size));

    return real_size;
}

// アクセストークンを取得する関数
auto sgRestGetAcessToken(const std::string_view& sg_url, const std::string_view& sg_script_name, 
    const std::string_view& sg_script_key, json* sg_auth_json) -> bool
{
    // CURL関連
    curl_ptr curl(curl_easy_init(), curl_deleter);
    CURLcode ret{};
    curl_slist* slist{nullptr};

    // HTTPレスポンスコード
    int http_code{0};

    // 出力データ
    std::stringstream write_data{};

    // ポストファイル
    std::string post_file{std::format("client_id={}&client_secret={}&grant_type=client_credentials"sv, sg_script_name, sg_script_key)};

    // ヘッダーを設定
    slist = curl_slist_append(slist, "Content-Type: application/x-www-form-urlencoded");
    slist = curl_slist_append(slist, "Accept: application/json");

    // オプションを設定
    curl_easy_setopt(curl.get(), CURLOPT_CUSTOMREQUEST, "POST");
    curl_easy_setopt(curl.get(), CURLOPT_BUFFERSIZE, 102400L);
    curl_easy_setopt(curl.get(), CURLOPT_URL, std::format("{}/api/v1/auth/access_token"sv, sg_url).c_str());
    curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, post_file.c_str());
    curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(post_file.size()));
    curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, slist);
    curl_easy_setopt(curl.get(), CURLOPT_MAXREDIRS, 50L);
    curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 0);
    curl_easy_setopt(curl.get(), CURLOPT_FTP_SKIP_PASV_IP, 1L);
    curl_easy_setopt(curl.get(), CURLOPT_TCP_KEEPALIVE, 1L);
    curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, &writeCallback);
    curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, static_cast<void*>(&write_data));

    // 実行
    ret = curl_easy_perform(curl.get());

    // レスポンスコード取得
    curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code);

    // 
    curl_slist_free_all(slist);
    slist = nullptr;

    // レスポンスコード200以外はエラー
    if ((ret != CURLE_OK) || (http_code != 200))
    {
        return false;
    }

    // 出力データをjsonにパース
    *sg_auth_json = json::parse(write_data);

    return true;
}

auto main() -> int
{
    // 初期化
    curl_global_init(CURL_GLOBAL_ALL);

    // アクセストークンjson
    json auth_json{};

    if (!sgRestGetAcessToken(SG_URL, SG_SCRIPT_NAME, SG_SCRIPT_KEY, &auth_json))
    {
        curl_global_cleanup();
        return 1;
    }

    auto token_type = auth_json["token_type"].get<std::string>();
    auto access_token = auth_json["access_token"].get<std::string>();

    // アクセストークンを表示
    std::cout << std::format("token_type : {}, access_token: {}"sv, token_type, access_token) << std::endl;

    // クリーンアップ
    curl_global_cleanup();

    return 0;
}
```

## レコードを検索

アクセストークンを取得するコードができたので次にレコードを検索するコードを作成します。
先ほどと同様にPythonでコマンドを実行します。

```python:curl_searching_record.py
import json
import subprocess

sg_url = '...'
sg_script_name = '...'
sg_script_key = '...'

curl_bin_dir = r'...'
out_cpp_file = r'...'

...
...

res = subprocess.check_output(curl_command)
auth_json = json.loads(res)

filters = {
    "filters": [["name", "contains", "..."]],
    "fields": ["name"]
}

curl_command = f'{curl_bin_dir}/curl '
curl_command += '-X POST '
curl_command += f'{sg_url}/api/v1/entity/projects/_search '
curl_command += f'-H "Content-Type: application/vnd+shotgun.api3_array+json" '
curl_command += f'-H "Accept: application/json" '
curl_command += f'-H "Authorization: {auth_json["token_type"]} {auth_json["access_token"]}" '
curl_command += f'-d "{json.dumps(filters)}" '
curl_command += f'--libcurl {out_cpp_file} '

res = subprocess.check_output(curl_command)
```

このコードはエラーが返りますが本題には関係ないので無視します。
また出力されたC++ファイルは省略します。

```cpp:searching_record.cpp
#include <format>
#include <memory>
#include <sstream>
#include <string>
#include <string_view>

#include <curl/curl.h>
#include <nlohmann/json.hpp>

// Shotgrid API関連の定数
static constexpr std::string_view SG_URL{"..."};
static constexpr std::string_view SG_SCRIPT_NAME{"..."};
static constexpr std::string_view SG_SCRIPT_KEY{"..."};

using json = nlohmann::json;

// 文字列リテラルを使用するため
using namespace std::literals::string_literals;
using namespace std::string_view_literals;

// CURL構造体のスマートポインタ
auto curl_deleter = [](CURL* _curl_ptr) { curl_easy_cleanup(_curl_ptr); };
using curl_ptr = std::unique_ptr<CURL, decltype(curl_deleter)>;

auto writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) -> size_t;

auto sgRestGetAcessToken(const std::string_view& sg_url, const std::string_view& sg_script_name,
    const std::string_view& sg_script_key, json* sg_auth_json) -> bool;

auto sgRestSearchRecord(const std::string_view& sg_url, const std::string_view& sg_entity, 
    const std::string_view& sg_token_type, const std::string_view& sg_access_token, const json& sg_filters_json, json* sg_result_json) -> bool
{
    // CURL関連
    curl_ptr curl(curl_easy_init(), curl_deleter);
    CURLcode ret{};
    curl_slist* slist{nullptr};

    // HTTPレスポンスコード
    int http_code{0};

    // フィルターデータおよび出力データ
    std::stringstream filters_data{}, write_data{};
    filters_data << sg_filters_json;

    // ポストファイル
    std::string post_file{filters_data.str()};

    // ヘッダーを設定 
    // TODO:Hashタイプのフィルターに対応
    slist = curl_slist_append(slist, "Content-Type: application/vnd+shotgun.api3_array+json");
    slist = curl_slist_append(slist, "Accept: application/json");
    slist = curl_slist_append(slist, std::format("Authorization: {} {}"sv, sg_token_type, sg_access_token).c_str());

    // オプションを設定
    curl_easy_setopt(curl.get(), CURLOPT_CUSTOMREQUEST, "POST");
    curl_easy_setopt(curl.get(), CURLOPT_BUFFERSIZE, 102400L);
    curl_easy_setopt(curl.get(), CURLOPT_URL, std::format("{}/api/v1/entity/{}/_search"sv, sg_url, sg_entity).c_str());
    curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, post_file.c_str());
    curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(post_file.size()));
    curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, slist);
    curl_easy_setopt(curl.get(), CURLOPT_MAXREDIRS, 50L);
    curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 0);
    curl_easy_setopt(curl.get(), CURLOPT_FTP_SKIP_PASV_IP, 1L);
    curl_easy_setopt(curl.get(), CURLOPT_TCP_KEEPALIVE, 1L);
    curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, &writeCallback);
    curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, static_cast<void*>(&write_data));

    // 実行
    ret = curl_easy_perform(curl.get());

    // レスポンスコード取得
    curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code);

    // 
    curl_slist_free_all(slist);
    slist = nullptr;

    // レスポンスコード200以外はエラー
    if ((ret != CURLE_OK) || (http_code != 200))
    {
        return false;
    }

    // 出力データをjsonにパース
    *sg_result_json = json::parse(write_data);

    return true;
}

auto main() -> int
{
    // 初期化
    curl_global_init(CURL_GLOBAL_ALL);

    // アクセストークン、検索出力、フィルターjson
    json auth_json{}, result_json{}, filters_json{};

    if (!sgRestGetAcessToken(SG_URL, SG_SCRIPT_NAME, SG_SCRIPT_KEY, &auth_json))
    {
        curl_global_cleanup();
        return 1;
    }

    auto token_type = auth_json["token_type"].get<std::string>();
    auto access_token = auth_json["access_token"].get<std::string>();

    // アクセストークンを表示
    std::cout << std::format("token_type : {}, access_token: {}"sv, token_type, access_token) << std::endl;

    // フィルターを設定
    filters_json["filters"] = {{"name", "contains", "..."}};
    filters_json["fields"] = {"name"};

    // フィルター内容を表示
    std::cout << filters_json << std::endl;

    if (!sgRestSearchRecord(SG_URL, "projects", token_type, access_token, filters_json, &result_json))
    {
        curl_global_cleanup();
        return 1;
    }

    // 取得結果を表示
    std::cout << result_json << std::endl;

    // クリーンアップ
    curl_global_cleanup();

    return 0;
}
```

上記コードはHashタイプのフィルターに対応していないためもし使用される場合はご注意ください。

## 終わりに

今後はUSDのAssetResolverに組み込む等を試すつもりです。
libcurlまわりのコードが少しわずらわしいのでC++ライクに書けるラッパーを用意しようかと思います。