Skip to main content

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}