Skip to main content

boj_client/query/
code.rs

1use crate::error::BojError;
2
3use super::options::{CsvEncoding, Format, Language};
4use super::validation::{validate_code, validate_date_generic, validate_date_order, validate_db};
5
6/// Query builder for the `getDataCode` endpoint.
7///
8/// Constraints enforced at build time:
9/// - `DB` must be non-empty ASCII and must not contain commas.
10/// - `CODE` entries must be non-empty ASCII, must not contain commas, and
11///   the number of entries must be between `1` and `1250`.
12/// - `startDate` and `endDate` must be `YYYY` or `YYYYXX` (`XX=01..12`), and
13///   if both are set
14///   they must have the same format and `startDate <= endDate`.
15///
16/// # Examples
17///
18/// ```
19/// use boj_client::query::{CodeQuery, Format, Language};
20///
21/// let _query = CodeQuery::new("CO", vec!["TK99F1000601GCQ01000".to_string()])?
22///     .with_format(Format::Json)
23///     .with_lang(Language::En)
24///     .with_start_date("202401")?
25///     .with_end_date("202402")?;
26/// # Ok::<(), boj_client::error::BojError>(())
27/// ```
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct CodeQuery {
30    db: String,
31    codes: Vec<String>,
32    format: Option<Format>,
33    lang: Option<Language>,
34    start_date: Option<String>,
35    end_date: Option<String>,
36    start_position: Option<u32>,
37}
38
39impl CodeQuery {
40    /// Creates a `getDataCode` query.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use boj_client::query::CodeQuery;
46    ///
47    /// let _query = CodeQuery::new("CO", vec!["TK99F1000601GCQ01000".to_string()])?;
48    /// # Ok::<(), boj_client::error::BojError>(())
49    /// ```
50    ///
51    /// # Errors
52    ///
53    /// Returns [`BojError`] if `db` or `codes` violate API constraints.
54    pub fn new(db: impl Into<String>, codes: Vec<String>) -> Result<Self, BojError> {
55        let db = db.into();
56        validate_db(&db)?;
57
58        if codes.is_empty() {
59            return Err(BojError::validation("CODE is required"));
60        }
61        if codes.len() > 1250 {
62            return Err(BojError::validation(
63                "CODE must contain 1250 or fewer series codes",
64            ));
65        }
66        for code in &codes {
67            validate_code(code)?;
68        }
69
70        Ok(Self {
71            db: db.to_ascii_uppercase(),
72            codes,
73            format: None,
74            lang: None,
75            start_date: None,
76            end_date: None,
77            start_position: None,
78        })
79    }
80
81    /// Sets the response format (`json` or `csv`).
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// use boj_client::query::{CodeQuery, Format};
87    ///
88    /// let _query = CodeQuery::new("CO", vec!["TK99F1000601GCQ01000".to_string()])?
89    ///     .with_format(Format::Csv);
90    /// # Ok::<(), boj_client::error::BojError>(())
91    /// ```
92    pub fn with_format(mut self, format: Format) -> Self {
93        self.format = Some(format);
94        self
95    }
96
97    /// Sets response language (`jp` or `en`).
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use boj_client::query::{CodeQuery, Language};
103    ///
104    /// let _query = CodeQuery::new("CO", vec!["TK99F1000601GCQ01000".to_string()])?
105    ///     .with_lang(Language::Jp);
106    /// # Ok::<(), boj_client::error::BojError>(())
107    /// ```
108    pub fn with_lang(mut self, lang: Language) -> Self {
109        self.lang = Some(lang);
110        self
111    }
112
113    /// Sets `startDate`.
114    ///
115    /// Accepted format is `YYYY` or `YYYYXX` (`XX=01..12`).
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use boj_client::query::CodeQuery;
121    ///
122    /// let _query = CodeQuery::new("CO", vec!["TK99F1000601GCQ01000".to_string()])?
123    ///     .with_start_date("2024")?;
124    /// # Ok::<(), boj_client::error::BojError>(())
125    /// ```
126    ///
127    /// # Errors
128    ///
129    /// Returns [`BojError`] if the value format is invalid or if `endDate`
130    /// is already set and the date order becomes invalid.
131    pub fn with_start_date(mut self, value: impl Into<String>) -> Result<Self, BojError> {
132        let value = value.into();
133        validate_date_generic(&value)?;
134        if let Some(end_date) = &self.end_date {
135            validate_date_order(&value, end_date)?;
136        }
137        self.start_date = Some(value);
138        Ok(self)
139    }
140
141    /// Sets `endDate`.
142    ///
143    /// Accepted format is `YYYY` or `YYYYXX` (`XX=01..12`).
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use boj_client::query::CodeQuery;
149    ///
150    /// let _query = CodeQuery::new("CO", vec!["TK99F1000601GCQ01000".to_string()])?
151    ///     .with_end_date("202402")?;
152    /// # Ok::<(), boj_client::error::BojError>(())
153    /// ```
154    ///
155    /// # Errors
156    ///
157    /// Returns [`BojError`] if the value format is invalid or if `startDate`
158    /// is already set and the date order becomes invalid.
159    pub fn with_end_date(mut self, value: impl Into<String>) -> Result<Self, BojError> {
160        let value = value.into();
161        validate_date_generic(&value)?;
162        if let Some(start_date) = &self.start_date {
163            validate_date_order(start_date, &value)?;
164        }
165        self.end_date = Some(value);
166        Ok(self)
167    }
168
169    /// Sets `startPosition`.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use boj_client::query::CodeQuery;
175    ///
176    /// let _query = CodeQuery::new("CO", vec!["TK99F1000601GCQ01000".to_string()])?
177    ///     .with_start_position(1)?;
178    /// # Ok::<(), boj_client::error::BojError>(())
179    /// ```
180    ///
181    /// # Errors
182    ///
183    /// Returns [`BojError`] when `start_position` is `0`.
184    pub fn with_start_position(mut self, start_position: u32) -> Result<Self, BojError> {
185        if start_position == 0 {
186            return Err(BojError::validation("STARTPOSITION must be >= 1"));
187        }
188        self.start_position = Some(start_position);
189        Ok(self)
190    }
191
192    pub(crate) fn endpoint(&self) -> &'static str {
193        "/api/v1/getDataCode"
194    }
195
196    pub(crate) fn query_pairs(&self) -> Vec<(String, String)> {
197        let mut pairs = Vec::new();
198        if let Some(format) = self.format {
199            pairs.push(("format".to_string(), format.as_query_value().to_string()));
200        }
201        if let Some(lang) = self.lang {
202            pairs.push(("lang".to_string(), lang.as_query_value().to_string()));
203        }
204        pairs.push(("db".to_string(), self.db.clone()));
205        if let Some(start_date) = &self.start_date {
206            pairs.push(("startDate".to_string(), start_date.clone()));
207        }
208        if let Some(end_date) = &self.end_date {
209            pairs.push(("endDate".to_string(), end_date.clone()));
210        }
211        pairs.push(("code".to_string(), self.codes.join(",")));
212        if let Some(start_position) = self.start_position {
213            pairs.push(("startPosition".to_string(), start_position.to_string()));
214        }
215        pairs
216    }
217
218    pub(crate) fn csv_encoding_hint(&self) -> CsvEncoding {
219        match self.lang.unwrap_or_default() {
220            Language::Jp => CsvEncoding::ShiftJis,
221            Language::En => CsvEncoding::Utf8,
222        }
223    }
224}