Redmine はプロジェクトの進行管理や不具合管理の機能を備えているオープンソースソフトウェアです。費用がかからず環境構築も容易であるため、様々なプロジェクトで使用されています。

Redmine は REST API を公開しており、外部のアプリケーションはこれを使うことで Redmine が管理している情報へ簡単にアクセスできます。これによって工数の集計を自動化したり、データを可視化するといったタスクを実現しやすくなります。またHTTP通信を行うためのライブラリは多くのプログラミング言語で提供されており、クライアントアプリケーションの開発を言語を問わず行うことができるのは REST API のメリットのひとつです。

そこで今回は、cpprestsdk という C++ 言語から REST API を利用するためのライブラリを使い、Redmine から自動的に工数データを集めてみます。

使用するツール

cpprestsdk

cpprestsdk は Microsoft がオープンソースで開発している、REST API のためのクロスプラットフォームな HTTP 通信ライブラリです。

非同期処理を前提としたモダンな API を持ち、特に GUI アプリケーションとの相性が良く、また JSON を扱う機能も内包しているためすぐに REST クライアントアプリケーションの開発を始めることができます。

vcpkg

vcpkg は Microsoft が開発している、クロスプラットフォームのオープンソース パッケージマネージャーです。今回は cpprestsdk をインストールするために使います。

vcpkg には cpprestsdk 以外にも、OpenCV や FFmpeg のようなメジャーなものから LLVM といった大規模なライブラリまで幅広く登録されています。さらにVisual Studio へ統合することで、従来は手動で設定していたインクルードパスやライブラリパスを自動的に解決してくれます。

作業環境

  • Windows10 x64
  • Visual Studio 2019
  • Git

vcpkg をインストールする

vcpkg を使うためには、ソースコードをダウンロードしてビルドする必要があります。と言っても難しい設定は不要です。バッチファイルを実行することでビルドできるようになっていますので、それを使います。

まずは vcpkg をダウンロードします。今後はこのダウンロード場所を vcpkg のインストールフォルダとして使用していきますので、”C:\tools\vcpkg” などわかりやすい場所にすると良いでしょう。
git clone を使い、タグの付いたバージョンを取得します。

git clone -b 2020.11 https://github.com/microsoft/vcpkg.git

ダウンロードしたフォルダ内にある bat ファイルを実行し、ビルドを開始します。

.\bootstrap-vcpkg.bat

成功すると vcpkg.exe が作成されます。動作確認として、バージョンを表示してみます。

.\vcpkg version
Vcpkg package management program version 2020.06.15-nohash

最後に Visual Studio へ統合します。これにより、インストールしたライブラリはインクルードパスなどを追加設定することなく使用できるようになります。

.\vcpkg integrate install

以上でインストールは完了となりますが、アンインストールする場合は “.\vcpkg integrate remove” を実行して統合を解除した後、vcpkg フォルダ自体を削除します。

cpprestsdk をインストールする

次のコマンドでインストールします。

.\vcpkg install cpprestsdk

コマンドを実行するとライブラリのソースコードがダウンロードされ、ビルドが始まります。

成功すると、vcpkg の installed フォルダ以下に必要なファイルが出力されます。

チケットの一覧を取得する

インストールが完了したら、まずは cpprestsdk と Redmine REST API の使い方を確認するため、あるプロジェクト内のチケットの一覧を取得してみます。

Redmine のチケットは、API 上では “issue” というデータ構造で表されます。リファレンスを参考に、”https://…/issues.json” のような URL へアクセスします。

VisualStudio でコンソールアプリケーションのプロジェクトを作成し、以下のプログラムを打ち込みます。なお実行する場合、事前に次の要素を定数にコピーしてください。

  • Redmine の URL → BASE_URL
  • Redmine の API キー → API_KEY
  • Redmine のプロジェクトID → PROJECT_ID
#include <iostream>
#include <clocale>
#include <cpprest/http_client.h>
#pragma comment(lib, "Crypt32.lib")
#pragma comment(lib, "Bcrypt.lib")
#pragma comment(lib, "Winhttp.lib")

using namespace web;
using namespace web::http;
using namespace web::http::client;
using namespace utility;

const string_t BASE_URL = L"https://...";  // Redmine の URL
const string_t API_KEY = L"...";           // API キー
const string_t PROJECT_ID = L"...";        // プロジェクトID


int main()
{
    // wchar_t を標準出力できるようにする
    std::setlocale(LC_ALL, "");

    // URL を作成する
    auto uri = uri_builder(L"issues.json")
        .append_query(L"key", API_KEY)
        .append_query(L"project_id", PROJECT_ID)
        .append_query(L"status_id", L"closed")
        .append_query(L"limit", L"100")
        .to_string();

    // チケットの一覧を取得して表示する
    auto client = http_client(BASE_URL);
    client.request(methods::GET, uri)   // GET リクエストを送信する
        .then([](http_response response) {   // レスポンスが返ってきたら、JSON データ構造を構築する
            return response.extract_json();
        })
        .then([](json::value json) {   // JSON データ構造の構築が完了したら、必要な情報を読み取る
            for (auto& issue : json[L"issues"].as_array()) {
                std::wcout << L"#" << issue[L"id"].as_integer() << " " << issue[L"subject"].as_string() << std::endl;
            }
        })
        .wait();   // 全ての処理が終わるまで待つ

    return 0;
}

実行結果は次のようになります。

#4674 見積テストチケット1
#4655 見積テストチケット2
#4652 見積テストチケット3
#4588 見積テストチケット4
#4587 見積テストチケット5

cpprestsdk の特徴的なところは、request() の戻り値が非同期で実行したい処理をつなげるための task<> クラスになっているところです。これに then() を使って、実行したい処理を順に設定していきます。

このスタイルは JavaScript の Fetch API とよく似ています。ただし、then() に指定した処理は別のスレッドからコールされます。今回はシンプルなコンソールアプリケーションであるためこの点を意識する必要はありませんが、実際にGUIアプリケーションへ組み込む際はデータアクセスの競合やプログレスバーなどGUIコントロールの操作に注意が必要です。

見積時間と実績時間を集計する

見積時間は  Issue のフィールドである “estimated_hours” から取得します。
実績時間は  Issue とは別の TimeEntries というデータとして保存されていますので、そこから取得します。

これらを集計し、見積に対する実績時間の多さをチケットごとにパーセンテージで表示してみます。

#include <iostream>
#include <clocale>
#include <cpprest/http_client.h>
#pragma comment(lib, "Crypt32.lib")
#pragma comment(lib, "Bcrypt.lib")
#pragma comment(lib, "Winhttp.lib")

using namespace web;
using namespace web::http;
using namespace web::http::client;
using namespace utility;

const string_t BASE_URL = L"https://...";  // Redmine の URL
const string_t API_KEY = L"...";           // API キー
const string_t PROJECT_ID = L"...";        // プロジェクトID

struct Issue
{
    int id;                     // チケットID
    string_t subject;           // チケットタイトル
    double estimated_hours;     // 見積工数
    double total_time_entiries; // 実績工数
};

int main()
{
    // wchar_t を標準出力できるようにする
    std::setlocale(LC_ALL, "");

    // URL を作成する
    auto uri = uri_builder(L"issues.json")
        .append_query(L"key", API_KEY)
        .append_query(L"project_id", PROJECT_ID)
        .append_query(L"limit", L"100")
        .append_query(L"status_id", L"closed")
        .to_string();

    // チケットの一覧を取得する
    auto client = http_client(BASE_URL);
    auto issues = client.request(methods::GET, uri) // GET リクエストを送信する
        .then([](http_response response) { // レスポンスが返ってきたら、JSON データ構造を作成する
            return response.extract_json();
        })
        .then([](json::value json) { // JSON データ構造の作成が完了したら、必要な情報を読み取る
            std::vector<Issue> result;
            for (auto& issue : json[L"issues"].as_array()) {
                result.push_back(Issue{
                    issue[L"id"].as_integer(),
                    issue[L"subject"].as_string(),
                    issue[L"estimated_hours"].is_double() ? issue[L"estimated_hours"].as_double() : 0.0,
                    0.0,
                });
            }
            return result;
        })
        .get(); // 全ての処理が終わるまで待つ。完了したら std::vector<Issue> が返ってくる
        
    // チケットごとに作業時間を集計する
    for (auto& issue : issues) {
        auto uri = uri_builder(L"time_entries.json")
            .append_query(L"key", API_KEY)
            .append_query(L"issue_id", std::to_wstring(issue.id))
            .append_query(L"status_id", L"closed")
            .append_query(L"limit", L"100")
            .to_string();

        issue.total_time_entiries = client.request(methods::GET, uri)
            .then([](http_response response) {
                return response.extract_json();
            })
            .then([](json::value json) {
                double total = 0.0;
                for (auto& time_entiry : json[L"time_entries"].as_array()) {
                    auto hours = time_entiry[L"hours"];
                    total += hours.is_double() ? hours.as_double() : 0.0;
                }
                return total;
            })
            .get();
    }

    // 結果を表示する
    for (auto& issue : issues) {
        std::wcout
            << "#" << issue.id << " "
            << issue.subject << " "
            << "(" << static_cast<int>(100 * (issue.total_time_entiries / issue.estimated_hours)) << "%) "
            << issue.total_time_entiries << "/" << issue.estimated_hours << " "
            << std::endl;
    }

    return 0;
}

実行結果は次のようになります。表示は、”(超過率%) 実績時間/見積時間” です。

#4959 見積テストチケット20    (100%) 1/1
#4957 見積テストチケット21    (58%) 3.5/6
#4955 見積テストチケット22    (62%) 1.25/2
#4953 見積テストチケット23    (87%) 1.75/2
#4951 見積テストチケット24    (112%) 2.25/2
#4946 見積テストチケット25    (87%) 1.75/2

Redmine の API で 100 件以上のデータを取得する際の注意点

今回は URL のクエリパラメータとして limit=100 を指定しており、一度に100件のデータを取得できます。しかし Redmine の制約として 100 件より多くのデータを一度に取得できません。

任意の範囲のデータを取得するには、API に “page” というクエリパラメータを指定します。100 件ずつ取得する場合は例えば、

  • 1~100 を取得するには “page=1”
  • 101~200 を取得するには “page=2”
  • 201~300 を取得するには “page=3”

・・・といったように、page 番号を指定しながら複数回のリクエストを送る必要があります。

例えば、Issue の取得は次のように修正できます。

int main()
{
    // wchar_t を標準出力できるようにする
    std::setlocale(LC_ALL, "");

    auto client = http_client(BASE_URL);

    // ページ数を取得する
    int page_count = 0;
    {
        auto uri = uri_builder(L"issues.json")
            .append_query(L"key", API_KEY)
            .append_query(L"project_id", PROJECT_ID)
            .append_query(L"status_id", L"closed")
            .to_string();

        page_count = client.request(methods::GET, uri)
            .then([](http_response response) {
                return response.extract_json();
            })
            .then([](json::value json) {
                return (json[L"total_count"].as_integer() / 100) + 1;
            })
            .get();
    }

    for (int i = 0; i < page_count; i++) {  // ページ数だけ繰り返す
        // URL を作成する
        auto uri = uri_builder(L"issues.json")
            .append_query(L"key", API_KEY)
            .append_query(L"project_id", PROJECT_ID)
            .append_query(L"status_id", L"closed")
            .append_query(L"limit", L"100")
            .append_query(L"page", std::to_wstring(i + 1))  // ページ番号を指定する
            .to_string();

        // チケットの一覧を取得する
        client.request(methods::GET, uri)
            .then([](http_response response) { 
                return response.extract_json();
            })
            .then([](json::value json) {
                for (auto& issue : json[L"issues"].as_array()) {
                    std::wcout << L"#" << issue[L"id"].as_integer() << " " << issue[L"subject"].as_string() << std::endl;
                }
            })
            .wait();
    }
 
    return 0;
}

まとめ

vcpkg と cpprestsdk を使って、Redmine から工数実績を取得してみました。

少し前までの C++ にはこのようなパッケージ管理やライブラリは無く、HTTP 通信をするだけでも、難しい環境構築や冗長なコードを書かざるを得ませんでした。しかし今回で、非常にシンプルなコードで REST API にアクセスできることが確認できました。

ただ今回のサンプルはコンソールアプリケーションであるため、視覚的な表現というところにはまだまだ及びません。グラフィカルなアプリケーションと組み合わせた表現の研究はこれからの課題となりそうです。

今後も、データの取り扱いの自動化やデータビジュアライゼーションについて検証していきます。

ハートランドのあたらしいテストツール「DT+シリーズ」
開発にあたり、本稿でも紹介している”Redmine”を使用しました!

動的、継承。DT+シリーズ、新登場。

今までのDTシリーズの機能はそのままに、
パーソナルなデバッグから、テストの自動化、リモートテストまで、
多様な開発スタイルに幅広く対応できる、進化する動的テストツール、
それがDT+。