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