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}