Skip to main content

boj_client/client/
core.rs

1use crate::decode::{decode_code, decode_layer, decode_metadata};
2use crate::error::BojError;
3use crate::model::{CodeResponse, LayerResponse, MetadataResponse};
4use crate::query::{CodeQuery, LayerQuery, MetadataQuery};
5use crate::transport::ReqwestTransport;
6
7use super::http::{execute_request, header_value};
8use super::response::{ensure_success_status, normalize_response_body};
9
10const DEFAULT_BASE_URL: &str = "https://www.stat-search.boj.or.jp";
11
12/// Synchronous BOJ API client.
13///
14/// `BojClient` uses an internal reqwest-based transport. Use [`BojClient::new`]
15/// for defaults or [`BojClient::with_reqwest_client`] to inject a customized
16/// reqwest client.
17///
18/// # Examples
19///
20/// ```no_run
21/// use boj_client::client::BojClient;
22/// use boj_client::query::{CodeQuery, Format, Language};
23///
24/// let client = BojClient::new()?;
25/// let query = CodeQuery::new("CO", vec!["TK99F1000601GCQ01000".to_string()])?
26///     .with_format(Format::Json)
27///     .with_lang(Language::En)
28///     .with_start_date("202401")?
29///     .with_end_date("202401")?;
30/// let _response = client.get_data_code(&query)?;
31/// # Ok::<(), boj_client::error::BojError>(())
32/// ```
33pub struct BojClient {
34    transport: ReqwestTransport,
35    base_url: String,
36}
37
38impl BojClient {
39    /// Creates a client with the default reqwest transport and BOJ base URL.
40    ///
41    /// The default transport uses:
42    /// - `User-Agent: boj-client/<crate-version>`
43    /// - request timeout of 30 seconds
44    ///
45    /// # Errors
46    ///
47    /// Returns [`BojError`] when building the internal reqwest client fails.
48    pub fn new() -> Result<Self, BojError> {
49        let client = ReqwestTransport::build_default_client()?;
50        Ok(Self {
51            transport: ReqwestTransport::new(client),
52            base_url: DEFAULT_BASE_URL.to_string(),
53        })
54    }
55
56    /// Creates a client from an existing `reqwest::blocking::Client`.
57    ///
58    /// This can be used to customize timeout, proxy, TLS, and other reqwest
59    /// settings while keeping the BOJ client API surface stable.
60    pub fn with_reqwest_client(client: reqwest::blocking::Client) -> Self {
61        Self {
62            transport: ReqwestTransport::new(client),
63            base_url: DEFAULT_BASE_URL.to_string(),
64        }
65    }
66
67    /// Replaces the base URL used for endpoint calls.
68    ///
69    /// This is mainly intended for tests and non-production environments.
70    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
71        self.base_url = base_url.into();
72        self
73    }
74
75    /// Calls `getDataCode` and decodes the response into [`CodeResponse`].
76    ///
77    /// # Errors
78    ///
79    /// Returns [`BojError`] when request sending fails, response decoding fails,
80    /// or when BOJ returns `STATUS != 200`.
81    pub fn get_data_code(&self, query: &CodeQuery) -> Result<CodeResponse, BojError> {
82        let response = execute_request(
83            &self.transport,
84            &self.base_url,
85            query.endpoint(),
86            query.query_pairs(),
87        )?;
88
89        let content_type = header_value(&response, "content-type");
90        let body = normalize_response_body(&response)?;
91        let decoded = decode_code(&body, content_type.as_deref(), query.csv_encoding_hint())?;
92
93        ensure_success_status(
94            decoded.meta.status,
95            &decoded.meta.message_id,
96            &decoded.meta.message,
97        )?;
98
99        Ok(decoded)
100    }
101
102    /// Calls `getDataLayer` and decodes the response into [`LayerResponse`].
103    ///
104    /// # Errors
105    ///
106    /// Returns [`BojError`] when request sending fails, response decoding fails,
107    /// or when BOJ returns `STATUS != 200`.
108    pub fn get_data_layer(&self, query: &LayerQuery) -> Result<LayerResponse, BojError> {
109        let response = execute_request(
110            &self.transport,
111            &self.base_url,
112            query.endpoint(),
113            query.query_pairs(),
114        )?;
115
116        let content_type = header_value(&response, "content-type");
117        let body = normalize_response_body(&response)?;
118        let decoded = decode_layer(&body, content_type.as_deref(), query.csv_encoding_hint())?;
119
120        ensure_success_status(
121            decoded.meta.status,
122            &decoded.meta.message_id,
123            &decoded.meta.message,
124        )?;
125
126        Ok(decoded)
127    }
128
129    /// Calls `getMetadata` and decodes the response into [`MetadataResponse`].
130    ///
131    /// # Errors
132    ///
133    /// Returns [`BojError`] when request sending fails, response decoding fails,
134    /// or when BOJ returns `STATUS != 200`.
135    pub fn get_metadata(&self, query: &MetadataQuery) -> Result<MetadataResponse, BojError> {
136        let response = execute_request(
137            &self.transport,
138            &self.base_url,
139            query.endpoint(),
140            query.query_pairs(),
141        )?;
142
143        let content_type = header_value(&response, "content-type");
144        let body = normalize_response_body(&response)?;
145        let decoded = decode_metadata(&body, content_type.as_deref(), query.csv_encoding_hint())?;
146
147        ensure_success_status(
148            decoded.meta.status,
149            &decoded.meta.message_id,
150            &decoded.meta.message,
151        )?;
152
153        Ok(decoded)
154    }
155}