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 Default for BojClient {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl BojClient {
45    /// Creates a client with the default reqwest transport and BOJ base URL.
46    pub fn new() -> Self {
47        Self {
48            transport: ReqwestTransport::default(),
49            base_url: DEFAULT_BASE_URL.to_string(),
50        }
51    }
52
53    /// Creates a client from an existing `reqwest::blocking::Client`.
54    ///
55    /// This can be used to customize timeout, proxy, TLS, and other reqwest
56    /// settings while keeping the BOJ client API surface stable.
57    pub fn with_reqwest_client(client: reqwest::blocking::Client) -> Self {
58        Self {
59            transport: ReqwestTransport::new(client),
60            base_url: DEFAULT_BASE_URL.to_string(),
61        }
62    }
63
64    /// Replaces the base URL used for endpoint calls.
65    ///
66    /// This is mainly intended for tests and non-production environments.
67    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
68        self.base_url = base_url.into();
69        self
70    }
71
72    /// Calls `getDataCode` and decodes the response into [`CodeResponse`].
73    ///
74    /// # Errors
75    ///
76    /// Returns [`BojError`] when request sending fails, response decoding fails,
77    /// or when BOJ returns `STATUS != 200`.
78    pub fn get_data_code(&self, query: &CodeQuery) -> Result<CodeResponse, BojError> {
79        let response = execute_request(
80            &self.transport,
81            &self.base_url,
82            query.endpoint(),
83            query.query_pairs(),
84        )?;
85
86        let content_type = header_value(&response, "content-type");
87        let body = normalize_response_body(&response)?;
88        let decoded = decode_code(&body, content_type.as_deref(), query.csv_encoding_hint())?;
89
90        ensure_success_status(
91            decoded.meta.status,
92            &decoded.meta.message_id,
93            &decoded.meta.message,
94        )?;
95
96        Ok(decoded)
97    }
98
99    /// Calls `getDataLayer` and decodes the response into [`LayerResponse`].
100    ///
101    /// # Errors
102    ///
103    /// Returns [`BojError`] when request sending fails, response decoding fails,
104    /// or when BOJ returns `STATUS != 200`.
105    pub fn get_data_layer(&self, query: &LayerQuery) -> Result<LayerResponse, BojError> {
106        let response = execute_request(
107            &self.transport,
108            &self.base_url,
109            query.endpoint(),
110            query.query_pairs(),
111        )?;
112
113        let content_type = header_value(&response, "content-type");
114        let body = normalize_response_body(&response)?;
115        let decoded = decode_layer(&body, content_type.as_deref(), query.csv_encoding_hint())?;
116
117        ensure_success_status(
118            decoded.meta.status,
119            &decoded.meta.message_id,
120            &decoded.meta.message,
121        )?;
122
123        Ok(decoded)
124    }
125
126    /// Calls `getMetadata` and decodes the response into [`MetadataResponse`].
127    ///
128    /// # Errors
129    ///
130    /// Returns [`BojError`] when request sending fails, response decoding fails,
131    /// or when BOJ returns `STATUS != 200`.
132    pub fn get_metadata(&self, query: &MetadataQuery) -> Result<MetadataResponse, BojError> {
133        let response = execute_request(
134            &self.transport,
135            &self.base_url,
136            query.endpoint(),
137            query.query_pairs(),
138        )?;
139
140        let content_type = header_value(&response, "content-type");
141        let body = normalize_response_body(&response)?;
142        let decoded = decode_metadata(&body, content_type.as_deref(), query.csv_encoding_hint())?;
143
144        ensure_success_status(
145            decoded.meta.status,
146            &decoded.meta.message_id,
147            &decoded.meta.message,
148        )?;
149
150        Ok(decoded)
151    }
152}