boj_client/query/layer.rs
1use crate::error::BojError;
2
3use super::options::{CsvEncoding, Format, Frequency, Language};
4use super::validation::{
5 parse_layer_value, validate_date_for_frequency, validate_date_order, validate_db,
6};
7
8/// A layer selector value for `getDataLayer`.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub(super) enum LayerValue {
11 /// Select all values in the layer (`*`).
12 Wildcard,
13 /// Select a specific positive layer index.
14 Index(u32),
15}
16
17impl LayerValue {
18 fn as_query_value(&self) -> String {
19 match self {
20 Self::Wildcard => "*".to_string(),
21 Self::Index(value) => value.to_string(),
22 }
23 }
24}
25
26/// Query builder for the `getDataLayer` endpoint.
27///
28/// Constraints enforced at build time:
29/// - `DB` must be non-empty ASCII and must not contain commas.
30/// - `LAYER` must contain between `1` and `5` entries.
31/// - Each layer entry must be either `*` or a positive integer.
32/// - Date format depends on `frequency`:
33/// - `CY` / `FY`: `YYYY`
34/// - `CH` / `FH`: `YYYYXX` where `XX` is `01..02`
35/// - `Q`: `YYYYXX` where `XX` is `01..04`
36/// - `M` / `W` / `D`: `YYYYXX` where `XX` is `01..12`
37/// - If both dates are set they must share the same format and satisfy
38/// `startDate <= endDate`.
39///
40/// # Examples
41///
42/// ```
43/// use boj_client::query::{Format, Frequency, Language, LayerQuery};
44///
45/// let _query = LayerQuery::new("BP01", Frequency::Q, vec!["1".to_string(), "*".to_string()])?
46/// .with_format(Format::Json)
47/// .with_lang(Language::En)
48/// .with_start_date("202401")?
49/// .with_end_date("202404")?;
50/// # Ok::<(), boj_client::error::BojError>(())
51/// ```
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct LayerQuery {
54 db: String,
55 frequency: Frequency,
56 layer: Vec<LayerValue>,
57 format: Option<Format>,
58 lang: Option<Language>,
59 start_date: Option<String>,
60 end_date: Option<String>,
61 start_position: Option<u32>,
62}
63
64impl LayerQuery {
65 /// Creates a `getDataLayer` query.
66 ///
67 /// # Examples
68 ///
69 /// ```
70 /// use boj_client::query::{Frequency, LayerQuery};
71 ///
72 /// let _query = LayerQuery::new("BP01", Frequency::M, vec!["1".to_string()])?;
73 /// # Ok::<(), boj_client::error::BojError>(())
74 /// ```
75 ///
76 /// # Errors
77 ///
78 /// Returns [`BojError`] if `db`, `layers`, or parsed layer values violate
79 /// API constraints.
80 pub fn new(
81 db: impl Into<String>,
82 frequency: Frequency,
83 layers: Vec<String>,
84 ) -> Result<Self, BojError> {
85 let db = db.into();
86 validate_db(&db)?;
87
88 if layers.is_empty() {
89 return Err(BojError::validation("LAYER is required"));
90 }
91 if layers.len() > 5 {
92 return Err(BojError::validation("LAYER accepts 1 to 5 levels only"));
93 }
94
95 let mut parsed_layers = Vec::with_capacity(layers.len());
96 for layer in &layers {
97 parsed_layers.push(parse_layer_value(layer)?);
98 }
99
100 Ok(Self {
101 db: db.to_ascii_uppercase(),
102 frequency,
103 layer: parsed_layers,
104 format: None,
105 lang: None,
106 start_date: None,
107 end_date: None,
108 start_position: None,
109 })
110 }
111
112 /// Sets the response format (`json` or `csv`).
113 ///
114 /// # Examples
115 ///
116 /// ```
117 /// use boj_client::query::{Format, Frequency, LayerQuery};
118 ///
119 /// let _query = LayerQuery::new("BP01", Frequency::M, vec!["1".to_string()])?
120 /// .with_format(Format::Csv);
121 /// # Ok::<(), boj_client::error::BojError>(())
122 /// ```
123 pub fn with_format(mut self, format: Format) -> Self {
124 self.format = Some(format);
125 self
126 }
127
128 /// Sets response language (`jp` or `en`).
129 ///
130 /// # Examples
131 ///
132 /// ```
133 /// use boj_client::query::{Frequency, Language, LayerQuery};
134 ///
135 /// let _query = LayerQuery::new("BP01", Frequency::M, vec!["1".to_string()])?
136 /// .with_lang(Language::Jp);
137 /// # Ok::<(), boj_client::error::BojError>(())
138 /// ```
139 pub fn with_lang(mut self, lang: Language) -> Self {
140 self.lang = Some(lang);
141 self
142 }
143
144 /// Sets `startDate` using the date format implied by `frequency`.
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// use boj_client::query::{Frequency, LayerQuery};
150 ///
151 /// let _query = LayerQuery::new("BP01", Frequency::Q, vec!["1".to_string()])?
152 /// .with_start_date("202401")?;
153 /// # Ok::<(), boj_client::error::BojError>(())
154 /// ```
155 ///
156 /// # Errors
157 ///
158 /// Returns [`BojError`] if the date format is invalid for the selected
159 /// `frequency`, or if `endDate` is already set and the date order is
160 /// invalid.
161 pub fn with_start_date(mut self, value: impl Into<String>) -> Result<Self, BojError> {
162 let value = value.into();
163 validate_date_for_frequency(&value, self.frequency)?;
164 if let Some(end_date) = &self.end_date {
165 validate_date_order(&value, end_date)?;
166 }
167 self.start_date = Some(value);
168 Ok(self)
169 }
170
171 /// Sets `endDate` using the date format implied by `frequency`.
172 ///
173 /// # Examples
174 ///
175 /// ```
176 /// use boj_client::query::{Frequency, LayerQuery};
177 ///
178 /// let _query = LayerQuery::new("BP01", Frequency::Q, vec!["1".to_string()])?
179 /// .with_end_date("202404")?;
180 /// # Ok::<(), boj_client::error::BojError>(())
181 /// ```
182 ///
183 /// # Errors
184 ///
185 /// Returns [`BojError`] if the date format is invalid for the selected
186 /// `frequency`, or if `startDate` is already set and the date order is
187 /// invalid.
188 pub fn with_end_date(mut self, value: impl Into<String>) -> Result<Self, BojError> {
189 let value = value.into();
190 validate_date_for_frequency(&value, self.frequency)?;
191 if let Some(start_date) = &self.start_date {
192 validate_date_order(start_date, &value)?;
193 }
194 self.end_date = Some(value);
195 Ok(self)
196 }
197
198 /// Sets `startPosition`.
199 ///
200 /// # Examples
201 ///
202 /// ```
203 /// use boj_client::query::{Frequency, LayerQuery};
204 ///
205 /// let _query = LayerQuery::new("BP01", Frequency::M, vec!["1".to_string()])?
206 /// .with_start_position(10)?;
207 /// # Ok::<(), boj_client::error::BojError>(())
208 /// ```
209 ///
210 /// # Errors
211 ///
212 /// Returns [`BojError`] when `start_position` is `0`.
213 pub fn with_start_position(mut self, start_position: u32) -> Result<Self, BojError> {
214 if start_position == 0 {
215 return Err(BojError::validation("STARTPOSITION must be >= 1"));
216 }
217 self.start_position = Some(start_position);
218 Ok(self)
219 }
220
221 pub(crate) fn endpoint(&self) -> &'static str {
222 "/api/v1/getDataLayer"
223 }
224
225 pub(crate) fn query_pairs(&self) -> Vec<(String, String)> {
226 let mut pairs = Vec::new();
227 if let Some(format) = self.format {
228 pairs.push(("format".to_string(), format.as_query_value().to_string()));
229 }
230 if let Some(lang) = self.lang {
231 pairs.push(("lang".to_string(), lang.as_query_value().to_string()));
232 }
233 pairs.push(("db".to_string(), self.db.clone()));
234 pairs.push((
235 "frequency".to_string(),
236 self.frequency.as_query_value().to_string(),
237 ));
238 pairs.push((
239 "layer".to_string(),
240 self.layer
241 .iter()
242 .map(LayerValue::as_query_value)
243 .collect::<Vec<_>>()
244 .join(","),
245 ));
246 if let Some(start_date) = &self.start_date {
247 pairs.push(("startDate".to_string(), start_date.clone()));
248 }
249 if let Some(end_date) = &self.end_date {
250 pairs.push(("endDate".to_string(), end_date.clone()));
251 }
252 if let Some(start_position) = self.start_position {
253 pairs.push(("startPosition".to_string(), start_position.to_string()));
254 }
255 pairs
256 }
257
258 pub(crate) fn csv_encoding_hint(&self) -> CsvEncoding {
259 match self.lang.unwrap_or_default() {
260 Language::Jp => CsvEncoding::ShiftJis,
261 Language::En => CsvEncoding::Utf8,
262 }
263 }
264}