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}