Skip to main content

ecbdp_api/
backend.rs

1use chrono::TimeZone;
2use serde::de::DeserializeOwned;
3use reqwest::{Client, Response, StatusCode};
4use crate::error::Error;
5use crate::query::{Resource, Query};
6use crate::parameter::{data::DataParameter, metadata::MetadataParameter};
7
8
9/// European Central Bank Data Portal data collection backend.
10/// 
11/// - For API details refrence: <https://data.ecb.europa.eu/help/api/overview>
12/// - For available data sets reference: <https://data.ecb.europa.eu/data/datasets>
13/// 
14/// Note: On the ECB Data Portal the series are prefixed with their flow identifier. For example,
15/// `EXR.M.USD.EUR.SP00.A` is the series key on the ECB Data Portal, but `EXR` is an `flow_id` and `M.USD.EUR.SP00.A`
16/// is the `series_key` in this implementation.
17pub struct ECBDataPortal;
18
19impl ECBDataPortal {
20    /// Checks the status code of the response for potential errors that can be converted into a `Rust` errors.
21    fn process_status_code(status_code: &StatusCode) -> Result<(), Error> {
22        match status_code.as_u16() {
23            400 => Err(Error::SC400),
24            404 => Err(Error::SC404),
25            406 => Err(Error::SC406),
26            500 => Err(Error::SC500),
27            501 => Err(Error::SC501),
28            503 => Err(Error::SC503),
29            _ => Ok((),)
30        }
31    }
32
33    /// Sends the request, and receive and process the response.
34    async fn process_request(url: &str) -> Result<String, Error> {
35        let user_agent: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.90 Safari/537.36";
36        let client: Client = Client::builder().user_agent(user_agent).build()?;
37        let response: Response = client.get(url).send().await?;
38        // Process status code
39        let status_code: StatusCode = response.status();
40        Self::process_status_code(&status_code)?;
41        // Return no data (Status code `304` in ECB Data Portal corresponds to no changes in the database since last request)
42        if status_code.as_u16() == 304 {
43            return Ok("[]".to_owned()); // Empty list
44        }
45        // Process response body
46        let response_body: String = response.text().await?;
47        Ok(response_body)
48    }
49
50    /// Sends a `data` resource request provided the constructed query and the list of parameters.
51    /// 
52    /// # Examples
53    /// 
54    /// ```rust
55    /// use chrono::{FixedOffset, DateTime, TimeZone};
56    /// use ecbdp_api::{ECBDataPortal, Query, FlowRef, DataParameter, ECBResponse};
57    /// use ecbdp_api::parameter::data::{Detail, Format};
58    /// 
59    /// #[tokio::main]
60    /// async fn main() -> () {
61    /// 
62    ///     // Query
63    ///     let q: Query = Query::new()
64    ///         .flow_ref(FlowRef { agency_id: None, flow_id: "EXR".to_owned(), version: None, })
65    ///         .series_key("M.USD.EUR.SP00.A");
66    ///     
67    ///     // Parameters
68    ///     let hour: i32 = 3600;
69    ///     let datetime: DateTime<FixedOffset> = FixedOffset::east_opt(1 * hour).unwrap()
70    ///         .with_ymd_and_hms(2009, 05, 15, 14, 15, 0).unwrap();
71    ///     let parameters: Option<Vec<DataParameter<FixedOffset>>> = Some(vec![
72    ///         DataParameter::UpdatedAfter { datetime, },
73    ///         DataParameter::Detail { detail: Detail::DataOnly, },
74    ///         DataParameter::Format { format: Format::JSONData, }
75    ///     ]);
76    ///     
77    ///     // Backend
78    ///     let ecb_response: ECBResponse = ECBDataPortal::get_data(&q, parameters).await.unwrap();
79    /// 
80    ///     assert!(0 < ecb_response.datasets[0].series.iter().last().unwrap().1.observations.as_ref().unwrap().len());
81    ///     assert_eq!(ecb_response.structure.name, "Exchange Rates".to_owned());
82    /// 
83    /// }
84    /// ```
85    pub async fn get_data<Tz, T>(q: &Query, parameters: Option<Vec<DataParameter<Tz>>>) -> Result<T, Error>
86    where
87        Tz: TimeZone,
88        <Tz as TimeZone>::Offset: std::fmt::Display,
89        T: DeserializeOwned,
90    {
91        q.validate_query(Resource::all_data_resources())?;
92        // Generate URL of the query (Including parameters)
93        let mut url: String = q.generate_url()? + "?";
94        if let Some(params) = parameters {
95            url += &params.iter().map(|p| p.to_string() ).collect::<Vec<String>>().join("&");
96        }
97        let response_body: String = Self::process_request(&url).await?;
98        let ecb_response: T = serde_json::from_str(&response_body)?;
99        Ok(ecb_response)
100    }
101
102    /// Sends a `schema` resource request provided the constructed query.
103    /// 
104    /// Note: The returned type is a `String`, which is just a text body of the response and it can be further deserialized
105    /// if the appropriate deserializetion traget `struct` is provided.
106    /// 
107    /// # Examples
108    /// 
109    /// ```rust
110    /// use ecbdp_api::{ECBDataPortal, Query, Resource, Context};
111    /// 
112    /// #[tokio::main]
113    /// async fn main() -> () {
114    /// 
115    ///     // Query
116    ///     let q: Query = Query::new()
117    ///         .resource(Resource::Schema)
118    ///         .context(Context::DataStructure)
119    ///         .agency_id("ECB")
120    ///         .resource_id("ECB_EXR1")
121    ///         .version("1.0");
122    /// 
123    ///     // Backend
124    ///     let schema: String = ECBDataPortal::get_schema(&q).await.unwrap();
125    /// 
126    ///     assert!(!schema.is_empty())
127    /// 
128    /// }
129    /// ```
130    pub async fn get_schema(q: &Query) -> Result<String, Error> {
131        q.validate_query(Resource::all_schema_resources())?;
132        let url: String = q.generate_url()?;
133        Self::process_request(&url).await
134    }
135
136    /// Sends a `metadata` resource request provided the constructed query and the list of parameters.
137    /// 
138    /// Note: The returned type is a `String`, which is just a text body of the response and it can be further deserialized
139    /// if the appropriate deserializetion traget `struct` is provided.
140    /// 
141    /// # Examples
142    /// 
143    /// ```rust
144    /// use ecbdp_api::{ECBDataPortal, Query, Resource};
145    /// 
146    /// #[tokio::main]
147    /// async fn main() -> () {
148    /// 
149    ///     // Query
150    ///     let q: Query = Query::new()
151    ///         .resource(Resource::MetadataDataStructure)
152    ///         .agency_id("ECB")
153    ///         .resource_id("ECB_EXR1")
154    ///         .version("latest");
155    /// 
156    ///     // Backend
157    ///     let metadata: String = ECBDataPortal::get_metadata(&q, None).await.unwrap();
158    /// 
159    ///     assert!(!metadata.is_empty())
160    /// 
161    /// }
162    /// ```
163    pub async fn get_metadata(q: &Query, parameters: Option<Vec<MetadataParameter>>) -> Result<String, Error> {
164        q.validate_query(Resource::all_metadata_resources())?;
165        // Generate URL of the query (Including parameters)
166        let mut url: String = q.generate_url()? + "?";
167        if let Some(params) = parameters {
168            url += &params.iter().map(|p| p.to_string() ).collect::<Vec<String>>().join("&");
169        }
170        Self::process_request(&url).await
171    }
172}
173
174
175#[cfg(test)]
176mod tests {
177    use chrono::{FixedOffset, DateTime, TimeZone};
178    use crate::backend::ECBDataPortal;
179    use crate::query;
180    use crate::parameter::data as pd;
181    use crate::schemas;
182
183    /// Functions that sets up the data for the unit tests.
184    async fn unit_test_set_up(flow_id: &str, series_key: &str) -> schemas::ECBResponse {
185        // Query
186        let q: query::Query = query::Query::new()
187            .flow_ref(query::FlowRef { agency_id: None, flow_id: flow_id.to_owned(), version: None, })
188            .series_key(series_key);
189        // Parameters
190        let hour: i32 = 3600;
191        let datetime: DateTime<FixedOffset> = FixedOffset::east_opt(1 * hour).unwrap()
192            .with_ymd_and_hms(2009, 05, 15, 14, 15, 0).unwrap();
193        let parameters: Option<Vec<pd::DataParameter<FixedOffset>>> = Some(vec![
194            pd::DataParameter::UpdatedAfter { datetime, },
195            pd::DataParameter::Detail { detail: pd::Detail::DataOnly, },
196            pd::DataParameter::Format { format: pd::Format::JSONData, }
197        ]);
198        // Backend
199        ECBDataPortal::get_data(&q, parameters).await.unwrap()
200    }
201
202    #[tokio::test]
203    async fn unit_test_get_data_1() -> () {
204        let ecb_response: schemas::ECBResponse = unit_test_set_up("EXR", "M.USD.EUR.SP00.A").await;
205        assert!(0 < ecb_response.datasets[0].series.iter().last().unwrap().1.observations.as_ref().unwrap().len());
206        assert_eq!(ecb_response.structure.name, "Exchange Rates".to_owned());
207    }
208
209    #[tokio::test]
210    async fn unit_test_get_data_2() -> () {
211        let ecb_response: schemas::ECBResponse = unit_test_set_up("FM", "B.U2.EUR.4F.KR.MLFR.LEV").await;
212        assert!(0 < ecb_response.datasets[0].series.iter().last().unwrap().1.observations.as_ref().unwrap().len());
213        assert_eq!(ecb_response.structure.name, "Financial market data".to_owned());
214    }
215
216    #[tokio::test]
217    async fn unit_test_get_data_3() -> () {
218        let ecb_response: schemas::ECBResponse = unit_test_set_up("CBD2", "Q.B0.W0.11._Z._Z.A.A.A0000._X.ALL.CA._Z.LE._T.EUR").await;
219        assert!(0 < ecb_response.datasets[0].series.iter().last().unwrap().1.observations.as_ref().unwrap().len());
220        assert_eq!(ecb_response.structure.name, "Consolidated Banking data".to_owned());
221    }
222
223    #[tokio::test]
224    async fn unit_test_get_data_4() -> () {
225        let ecb_response: schemas::ECBResponse = unit_test_set_up("PDD", "H.B0.W0.1._T.DDS_ALL._T._Z.N.PN").await;
226        assert!(0 < ecb_response.datasets[0].series.iter().last().unwrap().1.observations.as_ref().unwrap().len());
227        assert_eq!(ecb_response.structure.name, "PDD".to_owned());
228    }
229
230    #[tokio::test]
231    async fn unit_test_get_data_5() -> () {
232        let ecb_response: schemas::ECBResponse = unit_test_set_up("TGB", "M.U4.N.A094T.U2.EUR.A").await;
233        assert!(0 < ecb_response.datasets[0].series.iter().last().unwrap().1.observations.as_ref().unwrap().len());
234        assert_eq!(ecb_response.structure.name, "Target Balances".to_owned());
235    }
236
237    #[tokio::test]
238    async fn unit_test_get_schema() -> () {
239        // Query
240        let q: query::Query = query::Query::new()
241            .resource(query::Resource::Schema)
242            .context(query::Context::DataStructure)
243            .agency_id("ECB")
244            .resource_id("ECB_EXR1")
245            .version("1.0");
246        // Backend
247        let schema: String = ECBDataPortal::get_schema(&q).await.unwrap();
248        assert!(!schema.is_empty())
249    }
250
251    #[tokio::test]
252    async fn unit_test_get_metadata() -> () {
253        // Query
254        let q: query::Query = query::Query::new()
255            .resource(query::Resource::MetadataDataStructure)
256            .agency_id("ECB")
257            .resource_id("ECB_EXR1")
258            .version("latest");
259        // Backend
260        let metadata: String = ECBDataPortal::get_metadata(&q, None).await.unwrap();
261        assert!(!metadata.is_empty())
262    }
263}