gbiz-info-api 0.2.0

gBizINFO REST API (v2) を Rust から利用するためのクライアントライブラリ
Documentation
//! gBizINFO REST API (v2) のクライアント実装。
//!
//! 通常は [`GbizInfoClient::new`] で作成し、タイムアウト等を調整したい場合のみ
//! [`GbizInfoClient::builder`] を使う。

use std::future::{Future, IntoFuture};
use std::marker::PhantomData;
use std::pin::Pin;

use serde::de::DeserializeOwned;
use serde::Serialize;

use crate::error::GbizError;
use crate::query::{HojinSearchQuery, UpdateInfoQuery};
use crate::types::{
    HojinChildResponse, HojinResponse, SearchResponse, UpdateInfoChildResponse,
    UpdateInfoHojinResponse,
};

/// gBizINFO REST API (v2) のデフォルトのベース URL。
pub const DEFAULT_BASE_URL: &str = "https://api.info.gbiz.go.jp/hojin";

const API_TOKEN_HEADER: &str = "X-hojinInfo-api-token";

/// gBizINFO REST API (v2) クライアント。
///
/// API トークンは [Web API利用申請](https://info.gbiz.go.jp/hojin/various_registration/form)
/// で取得する。
///
/// クライアントは内部で [`reqwest::Client`] を保持しており、[`Clone`] してもコネクションプールは
/// 共有される。複数のリクエストで同じインスタンス(またはそのクローン)を使い回すこと。
///
/// ```no_run
/// use gbiz_info_api::{GbizInfoClient, HojinSearchQuery};
///
/// # async fn example() -> Result<(), gbiz_info_api::GbizError> {
/// let client = GbizInfoClient::new("YOUR_API_TOKEN");
/// let res = client
///     .search(&HojinSearchQuery::new().name("トヨタ自動車").limit(10))
///     .await?;
/// for info in res.into_hojin_infos() {
///     println!("{}", info.name.unwrap_or_default());
/// }
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct GbizInfoClient {
    http: reqwest::Client,
    base_url: String,
    api_token: String,
}

impl GbizInfoClient {
    /// API トークンを指定してクライアントを作成する。
    pub fn new(api_token: impl Into<String>) -> Self {
        Self::builder(api_token).build()
    }

    /// カスタム設定用のビルダーを作成する。
    pub fn builder(api_token: impl Into<String>) -> GbizInfoClientBuilder {
        GbizInfoClientBuilder {
            api_token: api_token.into(),
            base_url: None,
            http: None,
        }
    }

    /// 法人検索。
    ///
    /// `GET /v2/hojin`
    pub async fn search(&self, query: &HojinSearchQuery) -> Result<SearchResponse, GbizError> {
        self.get("/v2/hojin", query).await
    }

    /// 法人基本情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}`
    ///
    /// そのまま `.await` するか、`.metadata_flg(true).await` でメタデータ付きで取得する。
    pub fn hojin(&self, corporate_number: &str) -> DetailRequest<'_, HojinResponse> {
        self.detail(corporate_number, "")
    }

    /// 届出・認定情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}/certification`
    pub fn certification(&self, corporate_number: &str) -> DetailRequest<'_, HojinChildResponse> {
        self.detail(corporate_number, "/certification")
    }

    /// 表彰情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}/commendation`
    pub fn commendation(&self, corporate_number: &str) -> DetailRequest<'_, HojinChildResponse> {
        self.detail(corporate_number, "/commendation")
    }

    /// 事業所情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}/corporation`
    pub fn corporation(&self, corporate_number: &str) -> DetailRequest<'_, HojinChildResponse> {
        self.detail(corporate_number, "/corporation")
    }

    /// 財務情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}/finance`
    pub fn finance(&self, corporate_number: &str) -> DetailRequest<'_, HojinChildResponse> {
        self.detail(corporate_number, "/finance")
    }

    /// 特許情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}/patent`
    pub fn patent(&self, corporate_number: &str) -> DetailRequest<'_, HojinChildResponse> {
        self.detail(corporate_number, "/patent")
    }

    /// 調達情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}/procurement`
    pub fn procurement(&self, corporate_number: &str) -> DetailRequest<'_, HojinChildResponse> {
        self.detail(corporate_number, "/procurement")
    }

    /// 補助金情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}/subsidy`
    pub fn subsidy(&self, corporate_number: &str) -> DetailRequest<'_, HojinChildResponse> {
        self.detail(corporate_number, "/subsidy")
    }

    /// 職場情報の取得。
    ///
    /// `GET /v2/hojin/{corporate_number}/workplace`
    pub fn workplace(&self, corporate_number: &str) -> DetailRequest<'_, HojinChildResponse> {
        self.detail(corporate_number, "/workplace")
    }

    /// 期間内に追加/更新された法人基本情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo`
    pub async fn update_info(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoHojinResponse, GbizError> {
        self.get("/v2/hojin/updateInfo", query).await
    }

    /// 期間内に追加/更新された届出・認定情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo/certification`
    pub async fn update_info_certification(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoChildResponse, GbizError> {
        self.get("/v2/hojin/updateInfo/certification", query).await
    }

    /// 期間内に追加/更新された表彰情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo/commendation`
    pub async fn update_info_commendation(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoChildResponse, GbizError> {
        self.get("/v2/hojin/updateInfo/commendation", query).await
    }

    /// 期間内に追加/更新された事業所情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo/corporation`
    pub async fn update_info_corporation(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoChildResponse, GbizError> {
        self.get("/v2/hojin/updateInfo/corporation", query).await
    }

    /// 期間内に追加/更新された財務情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo/finance`
    pub async fn update_info_finance(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoChildResponse, GbizError> {
        self.get("/v2/hojin/updateInfo/finance", query).await
    }

    /// 期間内に追加/更新された特許情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo/patent`
    pub async fn update_info_patent(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoChildResponse, GbizError> {
        self.get("/v2/hojin/updateInfo/patent", query).await
    }

    /// 期間内に追加/更新された調達情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo/procurement`
    pub async fn update_info_procurement(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoChildResponse, GbizError> {
        self.get("/v2/hojin/updateInfo/procurement", query).await
    }

    /// 期間内に追加/更新された補助金情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo/subsidy`
    pub async fn update_info_subsidy(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoChildResponse, GbizError> {
        self.get("/v2/hojin/updateInfo/subsidy", query).await
    }

    /// 期間内に追加/更新された職場情報の取得。
    ///
    /// `GET /v2/hojin/updateInfo/workplace`
    pub async fn update_info_workplace(
        &self,
        query: &UpdateInfoQuery,
    ) -> Result<UpdateInfoChildResponse, GbizError> {
        self.get("/v2/hojin/updateInfo/workplace", query).await
    }

    fn detail<T>(&self, corporate_number: &str, suffix: &str) -> DetailRequest<'_, T> {
        DetailRequest {
            client: self,
            path: format!("/v2/hojin/{corporate_number}{suffix}"),
            metadata_flg: None,
            _response: PhantomData,
        }
    }

    async fn get<T, Q>(&self, path: &str, query: &Q) -> Result<T, GbizError>
    where
        T: DeserializeOwned,
        Q: Serialize + ?Sized,
    {
        let response = self
            .http
            .get(format!("{}{}", self.base_url, path))
            .header(API_TOKEN_HEADER, &self.api_token)
            .header(reqwest::header::ACCEPT, "application/json")
            .query(query)
            .send()
            .await?;
        let status = response.status();
        let body = response.text().await?;
        if !status.is_success() {
            return Err(GbizError::Status {
                status: status.as_u16(),
                body,
            });
        }
        serde_json::from_str(&body).map_err(|error| GbizError::Decode { error, body })
    }
}

/// [`GbizInfoClient`] のビルダー。
///
/// ```no_run
/// use std::time::Duration;
/// use gbiz_info_api::GbizInfoClient;
///
/// let http = reqwest::Client::builder()
///     .timeout(Duration::from_secs(30))
///     .build()
///     .unwrap();
/// let client = GbizInfoClient::builder("YOUR_API_TOKEN")
///     .http_client(http)
///     .build();
/// ```
#[derive(Debug)]
pub struct GbizInfoClientBuilder {
    api_token: String,
    base_url: Option<String>,
    http: Option<reqwest::Client>,
}

impl GbizInfoClientBuilder {
    /// ベース URL を上書きする(既定値: [`DEFAULT_BASE_URL`])。テストやプロキシ経由での利用向け。
    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
        self.base_url = Some(base_url.into());
        self
    }

    /// タイムアウト等を設定済みの [`reqwest::Client`] を利用する。
    pub fn http_client(mut self, http: reqwest::Client) -> Self {
        self.http = Some(http);
        self
    }

    /// クライアントを構築する。
    pub fn build(self) -> GbizInfoClient {
        GbizInfoClient {
            http: self.http.unwrap_or_default(),
            base_url: self
                .base_url
                .unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
            api_token: self.api_token,
        }
    }
}

/// 法人番号指定エンドポイントのリクエスト。
///
/// そのまま `.await` すると `metadata_flg` なしで送信される。
/// [`metadata_flg`](DetailRequest::metadata_flg) でメタデータ取得フラグを指定できる。
///
/// ```no_run
/// # use gbiz_info_api::GbizInfoClient;
/// # async fn example(client: GbizInfoClient) -> Result<(), gbiz_info_api::GbizError> {
/// // そのまま送信
/// let res = client.hojin("1180301018771").await?;
/// // メタデータ付きで送信
/// let res = client.hojin("1180301018771").metadata_flg(true).await?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
#[must_use = "リクエストは `.await` するまで送信されません"]
pub struct DetailRequest<'a, T> {
    client: &'a GbizInfoClient,
    path: String,
    metadata_flg: Option<bool>,
    _response: PhantomData<T>,
}

impl<'a, T> DetailRequest<'a, T>
where
    T: DeserializeOwned,
{
    /// メタデータ取得フラグを設定する。
    pub fn metadata_flg(mut self, value: bool) -> Self {
        self.metadata_flg = Some(value);
        self
    }

    /// リクエストを送信する(`.await` と等価)。
    pub async fn send(self) -> Result<T, GbizError> {
        let mut query: Vec<(&str, String)> = Vec::new();
        if let Some(flg) = self.metadata_flg {
            query.push(("metadata_flg", flg.to_string()));
        }
        self.client.get(&self.path, &query).await
    }
}

impl<'a, T> IntoFuture for DetailRequest<'a, T>
where
    T: DeserializeOwned + Send + 'a,
{
    type Output = Result<T, GbizError>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;

    fn into_future(self) -> Self::IntoFuture {
        Box::pin(self.send())
    }
}